@gratheon/log-lib 2.2.7 → 3.0.0

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
@@ -1,25 +1,24 @@
1
1
  # log-lib
2
2
 
3
- A TypeScript logging library with console output and MySQL database persistence support.
3
+ A TypeScript logging library with console output and Loki persistence support.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Dual Output**: Logs to both console and MySQL database
7
+ - **Dual Output**: Logs to both console and Loki
8
8
  - **Color-Coded Console**: ANSI colored output for different log levels
9
9
  - **Automatic Stacktrace Capture**: Every log includes file:line information
10
10
  - **CLI Output**: Shows the most relevant TypeScript file:line in gray color
11
- - **Database Storage**: Stores full stacktrace filtered to TypeScript files only
11
+ - **Loki Storage**: Stores full stacktrace filtered to TypeScript files only
12
12
  - **Enhanced Stack Traces**: Shows code frames with 5 lines of context around errors (dev mode)
13
13
  - **Error Cause Chain Tracking**: Traverses and displays the full error.cause chain
14
14
  - **Callsite Capture**: Captures where logger.error was called when error lacks stack frames
15
- - **Automatic Database Creation**: Creates logs database and table if they don't exist
16
- - **Non-blocking Initialization**: App starts even if logging DB fails
17
- - **Connection Pool Optimization**: Suppresses known MySQL warnings for cleaner logs
15
+ - **Non-blocking Delivery**: App starts even if Loki is unavailable
16
+ - **Service Labels**: Emits structured logs with `service`/`env` labels for Loki queries
18
17
  - **Global Exception Handlers**: Captures uncaught exceptions and unhandled rejections
19
18
  - **TypeScript Support**: Full type definitions included
20
19
  - **Fastify Integration**: Special logger interface for Fastify framework
21
20
  - **Flexible Metadata**: Support for structured metadata in logs
22
- - **Multiple Log Levels**: info, error, warn, debug (debug logs are not persisted to DB)
21
+ - **Multiple Log Levels**: info, error, warn, debug (debug logs are not persisted)
23
22
  - **Log Level Filtering**: Configure minimum log level via config or LOG_LEVEL env var
24
23
 
25
24
  ## Installation
@@ -28,16 +27,13 @@ A TypeScript logging library with console output and MySQL database persistence
28
27
  npm install @gratheon/log-lib
29
28
  ```
30
29
 
31
- ## Database Setup
30
+ ## Loki Setup
32
31
 
33
- The logger automatically creates the database and table on first initialization. No manual setup required!
32
+ By default the logger pushes to `http://loki:3100/loki/api/v1/push`.
34
33
 
35
- **Automatic Migrations**: When updating from older versions, the logger automatically runs migrations on initialization:
36
- - Checks if the `stacktrace` column exists
37
- - Adds it if missing (for v2.2.0+ compatibility)
38
- - Non-blocking: app starts even if migration fails
39
-
40
- For reference, migration scripts are available in the `migrations/` directory.
34
+ You can override with:
35
+ - `config.loki.url`
36
+ - `LOKI_URL` environment variable
41
37
 
42
38
  ## Usage
43
39
 
@@ -47,12 +43,10 @@ For reference, migration scripts are available in the `migrations/` directory.
47
43
  import { createLogger, LoggerConfig } from '@gratheon/log-lib';
48
44
 
49
45
  const config: LoggerConfig = {
50
- mysql: {
51
- host: 'localhost',
52
- port: 3306,
53
- user: 'your_user',
54
- password: 'your_password',
55
- database: 'logs' // optional, defaults to 'logs'
46
+ loki: {
47
+ url: 'http://loki:3100/loki/api/v1/push', // optional
48
+ service: 'user-cycle', // optional, defaults from env/current folder
49
+ labels: { team: 'platform' } // optional extra labels
56
50
  },
57
51
  logLevel: 'info' // optional, defaults to 'debug' in dev, 'info' in prod
58
52
  };
@@ -67,7 +61,7 @@ logger.warn('Low memory warning', { available: '100MB' });
67
61
  // Output: 12:34:56 [warn]: Low memory warning {"available":"100MB"} src/memory.ts:15
68
62
 
69
63
  logger.error('Failed to connect to API', { endpoint: '/api/users' });
70
- logger.debug('Processing item', { id: 123 }); // Not stored in DB
64
+ logger.debug('Processing item', { id: 123 }); // Not persisted
71
65
 
72
66
  // Error with stack trace and code frame (in dev mode)
73
67
  try {
@@ -114,11 +108,9 @@ import Fastify from 'fastify';
114
108
  import { createLogger, LoggerConfig } from '@gratheon/log-lib';
115
109
 
116
110
  const config: LoggerConfig = {
117
- mysql: {
118
- host: 'localhost',
119
- port: 3306,
120
- user: 'your_user',
121
- password: 'your_password'
111
+ loki: {
112
+ url: 'http://loki:3100/loki/api/v1/push',
113
+ service: 'my-fastify-service'
122
114
  }
123
115
  };
124
116
 
@@ -138,7 +130,7 @@ fastify.listen(3000);
138
130
  Creates and returns logger instances.
139
131
 
140
132
  **Parameters:**
141
- - `config`: Configuration object with MySQL connection details
133
+ - `config`: Configuration object with Loki details
142
134
 
143
135
  **Returns:**
144
136
  ```typescript
@@ -151,16 +143,16 @@ Creates and returns logger instances.
151
143
  ### Logger Methods
152
144
 
153
145
  #### `logger.info(message: string, meta?: LogMetadata)`
154
- Logs informational messages (console + DB)
146
+ Logs informational messages (console + Loki)
155
147
 
156
148
  #### `logger.error(message: string | Error, meta?: LogMetadata)`
157
- Logs errors with automatic Error object detection (console + DB)
149
+ Logs errors with automatic Error object detection (console + Loki)
158
150
 
159
151
  #### `logger.errorEnriched(message: string, error: Error, meta?: LogMetadata)`
160
- Logs enriched error messages with context (console + DB)
152
+ Logs enriched error messages with context (console + Loki)
161
153
 
162
154
  #### `logger.warn(message: string, meta?: LogMetadata)`
163
- Logs warning messages (console + DB)
155
+ Logs warning messages (console + Loki)
164
156
 
165
157
  #### `logger.debug(message: string, meta?: LogMetadata)`
166
158
  Logs debug messages (console only, not persisted)
@@ -178,30 +170,16 @@ Compatible with Fastify's logger interface:
178
170
 
179
171
  ## Advanced Features
180
172
 
181
- ### Connection Pool Configuration
173
+ ### Loki Delivery
182
174
 
183
- The logger uses an optimized connection pool:
184
- - Pool size: 3 connections
185
- - Max uses per connection: 200
186
- - Idle timeout: 30 seconds
187
- - Queue timeout: 60 seconds
188
- - Automatic error suppression for known MySQL warnings
175
+ The logger sends logs directly to Loki's HTTP push API (`/loki/api/v1/push`) in fire-and-forget mode.
189
176
 
190
177
  ### Message Truncation
191
178
 
192
- To prevent database bloat:
193
- - Messages are truncated to 2000 characters
194
- - Metadata is truncated to 2000 characters
179
+ To control payload size:
180
+ - Large log payloads are truncated before push
195
181
  - JSON stringification uses `fast-safe-stringify` for circular reference handling
196
182
 
197
- ### Async Initialization
198
-
199
- The logger initializes asynchronously in the background:
200
- ```typescript
201
- const { logger } = createLogger(config);
202
- logger.info('App starting'); // Works immediately, DB writes happen when ready
203
- ```
204
-
205
183
  ### Environment-Specific Behavior
206
184
 
207
185
  Set `ENV_ID` to control behavior:
@@ -217,35 +195,24 @@ Set `ENV_ID` to control behavior:
217
195
  - **Warn**: Yellow (level) + Magenta (metadata) + Gray (file:line)
218
196
  - **File Location**: Gray (file:line) - automatically captured from call stack
219
197
 
220
- ## Database Schema
221
-
222
- ```sql
223
- CREATE TABLE `logs` (
224
- `id` int auto_increment primary key,
225
- `level` varchar(16) not null,
226
- `message` varchar(2048) not null,
227
- `meta` varchar(2048) not null,
228
- `stacktrace` text,
229
- `timestamp` datetime not null
230
- );
231
- ```
198
+ ## Loki Payload
232
199
 
233
- The `stacktrace` column stores the full call stack filtered to TypeScript files only, making it easy to trace the origin of each log entry.
200
+ Each persisted entry is sent as JSON line data with:
201
+ - `timestamp`
202
+ - `level`
203
+ - `service`
204
+ - `message`
205
+ - `meta`
206
+ - `stacktrace`
234
207
 
235
208
  ## Error Handling
236
209
 
237
210
  The logger provides comprehensive error handling:
238
211
 
239
- ### Automatic Database Creation
240
- - Creates the `logs` database if it doesn't exist
241
- - Creates the `logs` table with proper schema and indexes
242
- - Non-blocking initialization - app starts even if DB fails
243
-
244
212
  ### Graceful Degradation
245
213
  - Logs are always written to console
246
- - Database errors are logged but don't crash the application
247
- - Connection pool errors are suppressed (packets out of order, inactivity warnings)
248
- - Fire-and-forget database logging (no await in hot path)
214
+ - Loki delivery errors are logged but don't crash the application
215
+ - Fire-and-forget Loki logging (no await in hot path)
249
216
 
250
217
  ### Enhanced Error Diagnostics
251
218
  - **Error Cause Chain**: Automatically traverses and displays `error.cause` chains
@@ -276,12 +243,14 @@ try {
276
243
 
277
244
  ```typescript
278
245
  interface LoggerConfig {
279
- mysql?: {
280
- host: string;
281
- port: number;
282
- user: string;
283
- password: string;
284
- database?: string; // defaults to 'logs'
246
+ loki?: {
247
+ url?: string; // defaults to process.env.LOKI_URL or http://loki:3100/loki/api/v1/push
248
+ service?: string; // defaults to process.env.SERVICE_NAME or cwd folder name
249
+ labels?: Record<string, string>;
250
+ username?: string;
251
+ password?: string;
252
+ tenantId?: string;
253
+ enabled?: boolean;
285
254
  };
286
255
  logLevel?: LogLevel; // 'debug' | 'info' | 'warn' | 'error', defaults to 'debug' in dev, 'info' in prod
287
256
  }
package/dist/logger.js CHANGED
@@ -28,13 +28,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
29
  exports.createLogger = void 0;
30
30
  require("source-map-support/register");
31
- const mysql_1 = __importStar(require("@databases/mysql"));
31
+ const http = __importStar(require("http"));
32
+ const https = __importStar(require("https"));
32
33
  const fast_safe_stringify_1 = __importDefault(require("fast-safe-stringify"));
33
34
  const fs = __importStar(require("fs"));
34
35
  const path = __importStar(require("path"));
35
- let conn = null;
36
- let dbInitialized = false;
37
- let initPromise = null;
36
+ let lokiConfig = null;
38
37
  const LOG_LEVELS = {
39
38
  debug: 0,
40
39
  info: 1,
@@ -66,71 +65,33 @@ function cleanStackTrace(stack) {
66
65
  });
67
66
  }).join('\n');
68
67
  }
69
- async function initializeConnection(config) {
70
- if (dbInitialized || !config.mysql)
71
- return;
72
- try {
73
- const database = config.mysql.database || 'logs';
74
- // First connect without database to create it if needed
75
- const tempConn = (0, mysql_1.default)({
76
- connectionString: `mysql://${config.mysql.user}:${config.mysql.password}@${config.mysql.host}:${config.mysql.port}/?connectionLimit=1&waitForConnections=true`,
77
- bigIntMode: 'number',
78
- });
79
- await tempConn.query((0, mysql_1.sql) `CREATE DATABASE IF NOT EXISTS ${mysql_1.sql.ident(database)} CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;`);
80
- await tempConn.dispose();
81
- // Now create the main connection pool with the logs database
82
- conn = (0, mysql_1.default)({
83
- connectionString: `mysql://${config.mysql.user}:${config.mysql.password}@${config.mysql.host}:${config.mysql.port}/${database}?connectionLimit=3&waitForConnections=true`,
84
- bigIntMode: 'number',
85
- poolSize: 3,
86
- maxUses: 200,
87
- idleTimeoutMilliseconds: 30000,
88
- queueTimeoutMilliseconds: 60000,
89
- onError: (err) => {
90
- // Suppress "packets out of order" and inactivity warnings
91
- if (!err.message?.includes('packets out of order') &&
92
- !err.message?.includes('inactivity') &&
93
- !err.message?.includes('wait_timeout')) {
94
- console.error(`MySQL logger connection pool error: ${err.message}`);
95
- }
96
- },
97
- });
98
- // Create logs table if it doesn't exist
99
- await conn.query((0, mysql_1.sql) `
100
- CREATE TABLE IF NOT EXISTS \`logs\` (
101
- id INT AUTO_INCREMENT PRIMARY KEY,
102
- level VARCHAR(50),
103
- message TEXT,
104
- meta TEXT,
105
- stacktrace TEXT,
106
- timestamp DATETIME,
107
- INDEX idx_timestamp (timestamp),
108
- INDEX idx_level (level)
109
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
110
- `);
111
- // Run migrations: Add stacktrace column if it doesn't exist (for existing tables created before v2.2.0)
112
- try {
113
- const columns = await conn.query((0, mysql_1.sql) `SHOW COLUMNS FROM \`logs\` LIKE 'stacktrace'`);
114
- if (columns.length === 0) {
115
- console.log('[log-lib] Running migration: Adding stacktrace column...');
116
- await conn.query((0, mysql_1.sql) `ALTER TABLE \`logs\` ADD COLUMN \`stacktrace\` TEXT AFTER \`meta\``);
117
- console.log('[log-lib] Migration complete: stacktrace column added');
118
- }
119
- else {
120
- console.log('[log-lib] Migration check: stacktrace column already exists');
121
- }
122
- }
123
- catch (migrationErr) {
124
- console.error('[log-lib] Migration failed (non-critical):', migrationErr);
125
- // Don't fail initialization if migration fails
126
- }
127
- dbInitialized = true;
128
- console.log('[log-lib] Database initialization complete');
129
- }
130
- catch (err) {
131
- console.error('Failed to initialize logs database:', err);
132
- // Don't throw - allow the service to start even if logging DB fails
68
+ function resolveLokiConfig(config) {
69
+ const explicit = config.loki ?? {};
70
+ const enabled = explicit.enabled ?? true;
71
+ if (!enabled) {
72
+ return null;
133
73
  }
74
+ const url = explicit.url || process.env.LOKI_URL || 'http://loki:3100/loki/api/v1/push';
75
+ const service = explicit.service ||
76
+ process.env.SERVICE_NAME ||
77
+ process.env.COMPOSE_SERVICE ||
78
+ process.env.npm_package_name ||
79
+ path.basename(process.cwd());
80
+ const labels = {
81
+ service,
82
+ env: process.env.ENV_ID || 'unknown',
83
+ logger: 'log-lib',
84
+ ...(explicit.labels || {}),
85
+ };
86
+ return {
87
+ enabled,
88
+ url,
89
+ service,
90
+ labels,
91
+ username: explicit.username,
92
+ password: explicit.password,
93
+ tenantId: explicit.tenantId,
94
+ };
134
95
  }
135
96
  function log(level, message, meta, fileLocation) {
136
97
  // Check if this log level should be filtered
@@ -316,40 +277,69 @@ function safeMeta(meta) {
316
277
  return {};
317
278
  return meta;
318
279
  }
319
- function storeInDB(level, message, meta, stacktrace) {
320
- if (!conn) {
321
- // Database not configured, skip DB logging
280
+ function storeInLoki(level, message, meta, stacktrace) {
281
+ if (!lokiConfig || !lokiConfig.enabled) {
322
282
  return;
323
283
  }
324
- // Wait for initialization to complete before writing
325
- const doStore = async () => {
326
- if (initPromise) {
327
- await initPromise.catch(() => { }); // Wait but ignore errors
284
+ try {
285
+ const nowNs = `${Date.now()}000000`;
286
+ const payloadObject = {
287
+ timestamp: new Date().toISOString(),
288
+ level,
289
+ service: lokiConfig.service,
290
+ message: safeToStringMessage(message),
291
+ meta: safeMeta(meta),
292
+ stacktrace: stacktrace || '',
293
+ };
294
+ const line = (0, fast_safe_stringify_1.default)(payloadObject).slice(0, 120000);
295
+ const body = (0, fast_safe_stringify_1.default)({
296
+ streams: [
297
+ {
298
+ stream: lokiConfig.labels,
299
+ values: [[nowNs, line]],
300
+ },
301
+ ],
302
+ });
303
+ const target = new URL(lokiConfig.url);
304
+ const isHttps = target.protocol === 'https:';
305
+ const client = isHttps ? https : http;
306
+ const headers = {
307
+ 'content-type': 'application/json',
308
+ 'content-length': Buffer.byteLength(body).toString(),
309
+ };
310
+ if (lokiConfig.tenantId) {
311
+ headers['X-Scope-OrgID'] = lokiConfig.tenantId;
328
312
  }
329
- if (!dbInitialized) {
330
- // Initialization failed, skip DB logging
331
- return;
313
+ if (lokiConfig.username && lokiConfig.password) {
314
+ const basic = Buffer.from(`${lokiConfig.username}:${lokiConfig.password}`).toString('base64');
315
+ headers['authorization'] = `Basic ${basic}`;
332
316
  }
333
- try {
334
- const msg = safeToStringMessage(message);
335
- const metaObj = safeMeta(meta);
336
- const metaStr = (0, fast_safe_stringify_1.default)(metaObj).slice(0, 2000);
337
- const stackStr = stacktrace || '';
338
- await conn.query((0, mysql_1.sql) `INSERT INTO \`logs\` (level, message, meta, stacktrace, timestamp) VALUES (${level}, ${msg}, ${metaStr}, ${stackStr}, NOW())`);
339
- }
340
- catch (e) {
341
- // fallback console output only - but don't spam
317
+ const req = client.request({
318
+ protocol: target.protocol,
319
+ hostname: target.hostname,
320
+ port: target.port || (isHttps ? 443 : 80),
321
+ path: `${target.pathname}${target.search}`,
322
+ method: 'POST',
323
+ headers,
324
+ }, (res) => {
325
+ if (res.statusCode && res.statusCode >= 400 && process.env.ENV_ID === 'dev') {
326
+ console.error(`[log-lib] Failed to persist log to Loki: HTTP ${res.statusCode}`);
327
+ }
328
+ res.resume();
329
+ });
330
+ req.on('error', (e) => {
342
331
  if (process.env.ENV_ID === 'dev') {
343
- console.error('Failed to persist log to DB', e);
332
+ console.error('[log-lib] Failed to persist log to Loki', e?.message || e);
344
333
  }
345
- }
346
- };
347
- // Fire and forget
348
- doStore().catch(e => {
334
+ });
335
+ req.write(body);
336
+ req.end();
337
+ }
338
+ catch (e) {
349
339
  if (process.env.ENV_ID === 'dev') {
350
- console.error('Unexpected failure preparing log for DB', e);
340
+ console.error('[log-lib] Unexpected failure preparing log for Loki', e?.message || e);
351
341
  }
352
- });
342
+ }
353
343
  }
354
344
  function createLogger(config = {}) {
355
345
  // Set up log level filtering
@@ -358,9 +348,9 @@ function createLogger(config = {}) {
358
348
  process.env.LOG_LEVEL ||
359
349
  (process.env.ENV_ID === 'dev' ? 'debug' : 'info');
360
350
  currentLogLevel = LOG_LEVELS[configuredLevel] ?? LOG_LEVELS.info;
361
- // Start initialization asynchronously (only if MySQL config provided)
362
- if (config.mysql) {
363
- initPromise = initializeConnection(config);
351
+ lokiConfig = resolveLokiConfig(config);
352
+ if (config.mysql && process.env.ENV_ID === 'dev') {
353
+ console.warn('[log-lib] `config.mysql` is deprecated and ignored. Logs are persisted to Loki.');
364
354
  }
365
355
  const logger = {
366
356
  info: (message, meta) => {
@@ -370,7 +360,7 @@ function createLogger(config = {}) {
370
360
  const frame = extractFirstProjectFrame(callStack);
371
361
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
372
362
  log('info', safeToStringMessage(message), metaObj, fileLocation);
373
- storeInDB('info', message, metaObj, fullTsStack);
363
+ storeInLoki('info', message, metaObj, fullTsStack);
374
364
  },
375
365
  error: (message, meta) => {
376
366
  const metaObj = safeMeta(meta);
@@ -387,9 +377,9 @@ function createLogger(config = {}) {
387
377
  if (causeChain.length) {
388
378
  console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
389
379
  }
390
- // For DB: include stack and error details in metadata
380
+ // For Loki: include stack and error details in metadata
391
381
  const enrichedMeta = { stack: message.stack, name: message.name, causeChain, ...metaObj };
392
- storeInDB('error', message.message, enrichedMeta, fullTsStack);
382
+ storeInLoki('error', message.message, enrichedMeta, fullTsStack);
393
383
  return;
394
384
  }
395
385
  const msgStr = safeToStringMessage(message);
@@ -399,7 +389,7 @@ function createLogger(config = {}) {
399
389
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
400
390
  log('error', msgStr, metaObj, fileLocation);
401
391
  printStackEnhanced(message);
402
- storeInDB('error', msgStr, metaObj, fullTsStack);
392
+ storeInLoki('error', msgStr, metaObj, fullTsStack);
403
393
  },
404
394
  errorEnriched: (message, error, meta) => {
405
395
  const metaObj = safeMeta(meta);
@@ -416,9 +406,9 @@ function createLogger(config = {}) {
416
406
  if (causeChain.length) {
417
407
  console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
418
408
  }
419
- // For DB: include stack and error details in metadata
409
+ // For Loki: include stack and error details in metadata
420
410
  const enrichedMeta = { stack: error.stack, name: error.name, causeChain, ...metaObj };
421
- storeInDB('error', `${message}: ${error.message}`, enrichedMeta, fullTsStack);
411
+ storeInLoki('error', `${message}: ${error.message}`, enrichedMeta, fullTsStack);
422
412
  return;
423
413
  }
424
414
  const errStr = safeToStringMessage(error);
@@ -428,7 +418,7 @@ function createLogger(config = {}) {
428
418
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
429
419
  log('error', `${message}: ${errStr}`, metaObj, fileLocation);
430
420
  printStackEnhanced(error);
431
- storeInDB('error', `${message}: ${errStr}`, metaObj, fullTsStack);
421
+ storeInLoki('error', `${message}: ${errStr}`, metaObj, fullTsStack);
432
422
  },
433
423
  warn: (message, meta) => {
434
424
  const metaObj = safeMeta(meta);
@@ -437,7 +427,7 @@ function createLogger(config = {}) {
437
427
  const frame = extractFirstProjectFrame(callStack);
438
428
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
439
429
  log('warn', safeToStringMessage(message), metaObj, fileLocation);
440
- storeInDB('warn', message, metaObj, fullTsStack);
430
+ storeInLoki('warn', message, metaObj, fullTsStack);
441
431
  },
442
432
  // do not store debug logs in DB
443
433
  debug: (message, meta) => {
@@ -455,7 +445,7 @@ function createLogger(config = {}) {
455
445
  const frame = extractFirstProjectFrame(callStack);
456
446
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
457
447
  log("info", messageString, undefined, fileLocation);
458
- // storeInDB("info", messageString); // Keep commented out as original
448
+ storeInLoki("info", messageString);
459
449
  },
460
450
  error: (msg, ...args) => {
461
451
  const errorMessage = (msg && msg.message) ? msg.message : String(msg);
@@ -465,8 +455,7 @@ function createLogger(config = {}) {
465
455
  const frame = extractFirstProjectFrame(callStack);
466
456
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
467
457
  log("error", errorMessage, meta, fileLocation);
468
- // Ensure string is passed to storeInDB
469
- storeInDB("error", typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : errorMessage, meta, fullTsStack);
458
+ storeInLoki("error", typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : errorMessage, meta, fullTsStack);
470
459
  },
471
460
  warn: (msg, ...args) => {
472
461
  const messageString = typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : String(msg);
@@ -475,7 +464,7 @@ function createLogger(config = {}) {
475
464
  const frame = extractFirstProjectFrame(callStack);
476
465
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
477
466
  log("warn", messageString, undefined, fileLocation);
478
- storeInDB("warn", messageString, undefined, fullTsStack); // Pass stringified message
467
+ storeInLoki("warn", messageString, undefined, fullTsStack);
479
468
  },
480
469
  // do not store debug logs in DB
481
470
  debug: (msg, ...args) => {
@@ -491,7 +480,7 @@ function createLogger(config = {}) {
491
480
  const frame = extractFirstProjectFrame(callStack);
492
481
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
493
482
  log("error", messageString, undefined, fileLocation);
494
- storeInDB("error", messageString, undefined, fullTsStack);
483
+ storeInLoki("error", messageString, undefined, fullTsStack);
495
484
  // Exit after a brief delay to allow logs to flush
496
485
  setTimeout(() => process.exit(1), 100);
497
486
  },
package/dist/types.d.ts CHANGED
@@ -1,5 +1,19 @@
1
1
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+ export interface LokiConfig {
3
+ url?: string;
4
+ username?: string;
5
+ password?: string;
6
+ tenantId?: string;
7
+ service?: string;
8
+ labels?: Record<string, string>;
9
+ enabled?: boolean;
10
+ }
2
11
  export interface LoggerConfig {
12
+ loki?: LokiConfig;
13
+ /**
14
+ * @deprecated MySQL persistence has been replaced by Loki.
15
+ * Kept only for backward compatibility and ignored.
16
+ */
3
17
  mysql?: {
4
18
  host: string;
5
19
  port: number;
package/example.ts CHANGED
@@ -2,13 +2,11 @@ import { createLogger, LoggerConfig } from './src/index';
2
2
 
3
3
  // Example configuration
4
4
  const config: LoggerConfig = {
5
- mysql: {
6
- host: 'localhost',
7
- port: 3306,
8
- user: 'root',
9
- password: 'test',
10
- database: 'logs' // optional, defaults to 'logs'
11
- }
5
+ loki: {
6
+ url: 'http://loki:3100/loki/api/v1/push',
7
+ service: 'example-service'
8
+ },
9
+ logLevel: 'info'
12
10
  };
13
11
 
14
12
  // Create logger instances
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gratheon/log-lib",
3
- "version": "2.2.7",
4
- "description": "Logging library with console and MySQL database persistence",
3
+ "version": "3.0.0",
4
+ "description": "Logging library with console output and Loki persistence",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
@@ -12,13 +12,12 @@
12
12
  "keywords": [
13
13
  "logging",
14
14
  "logger",
15
- "mysql",
15
+ "loki",
16
16
  "typescript"
17
17
  ],
18
- "author": "",
18
+ "author": "Artjom Kurapov",
19
19
  "license": "ISC",
20
20
  "dependencies": {
21
- "@databases/mysql": "^7.0.0",
22
21
  "fast-safe-stringify": "^2.1.1",
23
22
  "source-map-support": "^0.5.21"
24
23
  },
package/src/logger.ts CHANGED
@@ -1,13 +1,22 @@
1
1
  import 'source-map-support/register';
2
- import createConnectionPool, { sql, ConnectionPool } from "@databases/mysql";
2
+ import * as http from 'http';
3
+ import * as https from 'https';
3
4
  import jsonStringify from "fast-safe-stringify";
4
5
  import * as fs from 'fs';
5
6
  import * as path from 'path';
6
7
  import { LoggerConfig, Logger, FastifyLogger, LogMetadata, LogLevel } from "./types";
7
8
 
8
- let conn: ConnectionPool | null = null;
9
- let dbInitialized = false;
10
- let initPromise: Promise<void> | null = null;
9
+ type LokiRuntimeConfig = {
10
+ enabled: boolean;
11
+ url: string;
12
+ service: string;
13
+ labels: Record<string, string>;
14
+ username?: string;
15
+ password?: string;
16
+ tenantId?: string;
17
+ };
18
+
19
+ let lokiConfig: LokiRuntimeConfig | null = null;
11
20
 
12
21
  const LOG_LEVELS: Record<LogLevel, number> = {
13
22
  debug: 0,
@@ -45,74 +54,37 @@ function cleanStackTrace(stack: string): string {
45
54
  }).join('\n');
46
55
  }
47
56
 
48
- async function initializeConnection(config: LoggerConfig) {
49
- if (dbInitialized || !config.mysql) return;
50
-
51
- try {
52
- const database = config.mysql.database || 'logs';
53
-
54
- // First connect without database to create it if needed
55
- const tempConn = createConnectionPool({
56
- connectionString: `mysql://${config.mysql.user}:${config.mysql.password}@${config.mysql.host}:${config.mysql.port}/?connectionLimit=1&waitForConnections=true`,
57
- bigIntMode: 'number',
58
- });
59
-
60
- await tempConn.query(sql`CREATE DATABASE IF NOT EXISTS ${sql.ident(database)} CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;`);
61
- await tempConn.dispose();
62
-
63
- // Now create the main connection pool with the logs database
64
- conn = createConnectionPool({
65
- connectionString: `mysql://${config.mysql.user}:${config.mysql.password}@${config.mysql.host}:${config.mysql.port}/${database}?connectionLimit=3&waitForConnections=true`,
66
- bigIntMode: 'number',
67
- poolSize: 3,
68
- maxUses: 200,
69
- idleTimeoutMilliseconds: 30_000,
70
- queueTimeoutMilliseconds: 60_000,
71
- onError: (err) => {
72
- // Suppress "packets out of order" and inactivity warnings
73
- if (!err.message?.includes('packets out of order') &&
74
- !err.message?.includes('inactivity') &&
75
- !err.message?.includes('wait_timeout')) {
76
- console.error(`MySQL logger connection pool error: ${err.message}`);
77
- }
78
- },
79
- });
80
-
81
- // Create logs table if it doesn't exist
82
- await conn.query(sql`
83
- CREATE TABLE IF NOT EXISTS \`logs\` (
84
- id INT AUTO_INCREMENT PRIMARY KEY,
85
- level VARCHAR(50),
86
- message TEXT,
87
- meta TEXT,
88
- stacktrace TEXT,
89
- timestamp DATETIME,
90
- INDEX idx_timestamp (timestamp),
91
- INDEX idx_level (level)
92
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
93
- `);
94
-
95
- // Run migrations: Add stacktrace column if it doesn't exist (for existing tables created before v2.2.0)
96
- try {
97
- const columns = await conn.query(sql`SHOW COLUMNS FROM \`logs\` LIKE 'stacktrace'`);
98
- if (columns.length === 0) {
99
- console.log('[log-lib] Running migration: Adding stacktrace column...');
100
- await conn.query(sql`ALTER TABLE \`logs\` ADD COLUMN \`stacktrace\` TEXT AFTER \`meta\``);
101
- console.log('[log-lib] Migration complete: stacktrace column added');
102
- } else {
103
- console.log('[log-lib] Migration check: stacktrace column already exists');
104
- }
105
- } catch (migrationErr) {
106
- console.error('[log-lib] Migration failed (non-critical):', migrationErr);
107
- // Don't fail initialization if migration fails
108
- }
109
-
110
- dbInitialized = true;
111
- console.log('[log-lib] Database initialization complete');
112
- } catch (err) {
113
- console.error('Failed to initialize logs database:', err);
114
- // Don't throw - allow the service to start even if logging DB fails
57
+ function resolveLokiConfig(config: LoggerConfig): LokiRuntimeConfig | null {
58
+ const explicit = config.loki ?? {};
59
+ const enabled = explicit.enabled ?? true;
60
+ if (!enabled) {
61
+ return null;
115
62
  }
63
+
64
+ const url = explicit.url || process.env.LOKI_URL || 'http://loki:3100/loki/api/v1/push';
65
+ const service =
66
+ explicit.service ||
67
+ process.env.SERVICE_NAME ||
68
+ process.env.COMPOSE_SERVICE ||
69
+ process.env.npm_package_name ||
70
+ path.basename(process.cwd());
71
+
72
+ const labels: Record<string, string> = {
73
+ service,
74
+ env: process.env.ENV_ID || 'unknown',
75
+ logger: 'log-lib',
76
+ ...(explicit.labels || {}),
77
+ };
78
+
79
+ return {
80
+ enabled,
81
+ url,
82
+ service,
83
+ labels,
84
+ username: explicit.username,
85
+ password: explicit.password,
86
+ tenantId: explicit.tenantId,
87
+ };
116
88
  }
117
89
 
118
90
  function log(level: string, message: string, meta?: any, fileLocation?: string) {
@@ -297,44 +269,78 @@ function safeMeta(meta: any): any {
297
269
  return meta;
298
270
  }
299
271
 
300
- function storeInDB(level: string, message: any, meta?: any, stacktrace?: string) {
301
- if (!conn) {
302
- // Database not configured, skip DB logging
272
+ function storeInLoki(level: LogLevel, message: any, meta?: any, stacktrace?: string) {
273
+ if (!lokiConfig || !lokiConfig.enabled) {
303
274
  return;
304
275
  }
305
-
306
- // Wait for initialization to complete before writing
307
- const doStore = async () => {
308
- if (initPromise) {
309
- await initPromise.catch(() => {}); // Wait but ignore errors
276
+
277
+ try {
278
+ const nowNs = `${Date.now()}000000`;
279
+ const payloadObject = {
280
+ timestamp: new Date().toISOString(),
281
+ level,
282
+ service: lokiConfig.service,
283
+ message: safeToStringMessage(message),
284
+ meta: safeMeta(meta),
285
+ stacktrace: stacktrace || '',
286
+ };
287
+
288
+ const line = jsonStringify(payloadObject).slice(0, 120_000);
289
+ const body = jsonStringify({
290
+ streams: [
291
+ {
292
+ stream: lokiConfig.labels,
293
+ values: [[nowNs, line]],
294
+ },
295
+ ],
296
+ });
297
+
298
+ const target = new URL(lokiConfig.url);
299
+ const isHttps = target.protocol === 'https:';
300
+ const client = isHttps ? https : http;
301
+ const headers: Record<string, string> = {
302
+ 'content-type': 'application/json',
303
+ 'content-length': Buffer.byteLength(body).toString(),
304
+ };
305
+
306
+ if (lokiConfig.tenantId) {
307
+ headers['X-Scope-OrgID'] = lokiConfig.tenantId;
310
308
  }
311
-
312
- if (!dbInitialized) {
313
- // Initialization failed, skip DB logging
314
- return;
309
+ if (lokiConfig.username && lokiConfig.password) {
310
+ const basic = Buffer.from(`${lokiConfig.username}:${lokiConfig.password}`).toString('base64');
311
+ headers['authorization'] = `Basic ${basic}`;
315
312
  }
316
-
317
- try {
318
- const msg = safeToStringMessage(message);
319
- const metaObj = safeMeta(meta);
320
- const metaStr = jsonStringify(metaObj).slice(0, 2000);
321
- const stackStr = stacktrace || '';
322
-
323
- await conn!.query(sql`INSERT INTO \`logs\` (level, message, meta, stacktrace, timestamp) VALUES (${level}, ${msg}, ${metaStr}, ${stackStr}, NOW())`);
324
- } catch (e: any) {
325
- // fallback console output only - but don't spam
313
+
314
+ const req = client.request(
315
+ {
316
+ protocol: target.protocol,
317
+ hostname: target.hostname,
318
+ port: target.port || (isHttps ? 443 : 80),
319
+ path: `${target.pathname}${target.search}`,
320
+ method: 'POST',
321
+ headers,
322
+ },
323
+ (res) => {
324
+ if (res.statusCode && res.statusCode >= 400 && process.env.ENV_ID === 'dev') {
325
+ console.error(`[log-lib] Failed to persist log to Loki: HTTP ${res.statusCode}`);
326
+ }
327
+ res.resume();
328
+ }
329
+ );
330
+
331
+ req.on('error', (e: any) => {
326
332
  if (process.env.ENV_ID === 'dev') {
327
- console.error('Failed to persist log to DB', e);
333
+ console.error('[log-lib] Failed to persist log to Loki', e?.message || e);
328
334
  }
329
- }
330
- };
331
-
332
- // Fire and forget
333
- doStore().catch(e => {
335
+ });
336
+
337
+ req.write(body);
338
+ req.end();
339
+ } catch (e: any) {
334
340
  if (process.env.ENV_ID === 'dev') {
335
- console.error('Unexpected failure preparing log for DB', e);
341
+ console.error('[log-lib] Unexpected failure preparing log for Loki', e?.message || e);
336
342
  }
337
- });
343
+ }
338
344
  }
339
345
 
340
346
  export function createLogger(config: LoggerConfig = {}): { logger: Logger; fastifyLogger: FastifyLogger } {
@@ -346,9 +352,9 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
346
352
 
347
353
  currentLogLevel = LOG_LEVELS[configuredLevel] ?? LOG_LEVELS.info;
348
354
 
349
- // Start initialization asynchronously (only if MySQL config provided)
350
- if (config.mysql) {
351
- initPromise = initializeConnection(config);
355
+ lokiConfig = resolveLokiConfig(config);
356
+ if (config.mysql && process.env.ENV_ID === 'dev') {
357
+ console.warn('[log-lib] `config.mysql` is deprecated and ignored. Logs are persisted to Loki.');
352
358
  }
353
359
 
354
360
  const logger: Logger = {
@@ -360,7 +366,7 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
360
366
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
361
367
 
362
368
  log('info', safeToStringMessage(message), metaObj, fileLocation);
363
- storeInDB('info', message, metaObj, fullTsStack);
369
+ storeInLoki('info', message, metaObj, fullTsStack);
364
370
  },
365
371
  error: (message: string | Error | any, meta?: LogMetadata) => {
366
372
  const metaObj = safeMeta(meta);
@@ -379,9 +385,9 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
379
385
  console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
380
386
  }
381
387
 
382
- // For DB: include stack and error details in metadata
388
+ // For Loki: include stack and error details in metadata
383
389
  const enrichedMeta = {stack: message.stack, name: message.name, causeChain, ...metaObj};
384
- storeInDB('error', message.message, enrichedMeta, fullTsStack);
390
+ storeInLoki('error', message.message, enrichedMeta, fullTsStack);
385
391
  return;
386
392
  }
387
393
  const msgStr = safeToStringMessage(message);
@@ -392,7 +398,7 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
392
398
 
393
399
  log('error', msgStr, metaObj, fileLocation);
394
400
  printStackEnhanced(message);
395
- storeInDB('error', msgStr, metaObj, fullTsStack);
401
+ storeInLoki('error', msgStr, metaObj, fullTsStack);
396
402
  },
397
403
  errorEnriched: (message: string, error: Error | any, meta?: LogMetadata) => {
398
404
  const metaObj = safeMeta(meta);
@@ -411,9 +417,9 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
411
417
  console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
412
418
  }
413
419
 
414
- // For DB: include stack and error details in metadata
420
+ // For Loki: include stack and error details in metadata
415
421
  const enrichedMeta = {stack: error.stack, name: error.name, causeChain, ...metaObj};
416
- storeInDB('error', `${message}: ${error.message}`, enrichedMeta, fullTsStack);
422
+ storeInLoki('error', `${message}: ${error.message}`, enrichedMeta, fullTsStack);
417
423
  return;
418
424
  }
419
425
  const errStr = safeToStringMessage(error);
@@ -424,7 +430,7 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
424
430
 
425
431
  log('error', `${message}: ${errStr}`, metaObj, fileLocation);
426
432
  printStackEnhanced(error);
427
- storeInDB('error', `${message}: ${errStr}`, metaObj, fullTsStack);
433
+ storeInLoki('error', `${message}: ${errStr}`, metaObj, fullTsStack);
428
434
  },
429
435
  warn: (message: string, meta?: LogMetadata) => {
430
436
  const metaObj = safeMeta(meta);
@@ -434,7 +440,7 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
434
440
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
435
441
 
436
442
  log('warn', safeToStringMessage(message), metaObj, fileLocation);
437
- storeInDB('warn', message, metaObj, fullTsStack);
443
+ storeInLoki('warn', message, metaObj, fullTsStack);
438
444
  },
439
445
 
440
446
  // do not store debug logs in DB
@@ -456,7 +462,7 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
456
462
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
457
463
 
458
464
  log("info", messageString, undefined, fileLocation);
459
- // storeInDB("info", messageString); // Keep commented out as original
465
+ storeInLoki("info", messageString);
460
466
  },
461
467
  error: (msg: any, ...args: any[]) => {
462
468
  const errorMessage = (msg && msg.message) ? msg.message : String(msg);
@@ -467,8 +473,7 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
467
473
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
468
474
 
469
475
  log("error", errorMessage, meta, fileLocation);
470
- // Ensure string is passed to storeInDB
471
- storeInDB("error", typeof msg === 'object' ? jsonStringify(msg) : errorMessage, meta, fullTsStack);
476
+ storeInLoki("error", typeof msg === 'object' ? jsonStringify(msg) : errorMessage, meta, fullTsStack);
472
477
  },
473
478
  warn: (msg: any, ...args: any[]) => {
474
479
  const messageString = typeof msg === 'object' ? jsonStringify(msg) : String(msg);
@@ -478,7 +483,7 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
478
483
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
479
484
 
480
485
  log("warn", messageString, undefined, fileLocation);
481
- storeInDB("warn", messageString, undefined, fullTsStack); // Pass stringified message
486
+ storeInLoki("warn", messageString, undefined, fullTsStack);
482
487
  },
483
488
 
484
489
  // do not store debug logs in DB
@@ -498,7 +503,7 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
498
503
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
499
504
 
500
505
  log("error", messageString, undefined, fileLocation);
501
- storeInDB("error", messageString, undefined, fullTsStack);
506
+ storeInLoki("error", messageString, undefined, fullTsStack);
502
507
  // Exit after a brief delay to allow logs to flush
503
508
  setTimeout(() => process.exit(1), 100);
504
509
  },
package/src/types.ts CHANGED
@@ -1,6 +1,21 @@
1
1
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
2
 
3
+ export interface LokiConfig {
4
+ url?: string; // defaults to process.env.LOKI_URL or http://loki:3100/loki/api/v1/push
5
+ username?: string;
6
+ password?: string;
7
+ tenantId?: string;
8
+ service?: string; // defaults to process.env.SERVICE_NAME or current folder name
9
+ labels?: Record<string, string>;
10
+ enabled?: boolean; // defaults to true
11
+ }
12
+
3
13
  export interface LoggerConfig {
14
+ loki?: LokiConfig;
15
+ /**
16
+ * @deprecated MySQL persistence has been replaced by Loki.
17
+ * Kept only for backward compatibility and ignored.
18
+ */
4
19
  mysql?: {
5
20
  host: string;
6
21
  port: number;