@gratheon/log-lib 2.2.6 → 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,12 +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;
36
+ let lokiConfig = null;
37
37
  const LOG_LEVELS = {
38
38
  debug: 0,
39
39
  info: 1,
@@ -65,71 +65,33 @@ function cleanStackTrace(stack) {
65
65
  });
66
66
  }).join('\n');
67
67
  }
68
- async function initializeConnection(config) {
69
- if (dbInitialized || !config.mysql)
70
- return;
71
- try {
72
- const database = config.mysql.database || 'logs';
73
- // First connect without database to create it if needed
74
- const tempConn = (0, mysql_1.default)({
75
- connectionString: `mysql://${config.mysql.user}:${config.mysql.password}@${config.mysql.host}:${config.mysql.port}/?connectionLimit=1&waitForConnections=true`,
76
- bigIntMode: 'number',
77
- });
78
- await tempConn.query((0, mysql_1.sql) `CREATE DATABASE IF NOT EXISTS ${mysql_1.sql.ident(database)} CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;`);
79
- await tempConn.dispose();
80
- // Now create the main connection pool with the logs database
81
- conn = (0, mysql_1.default)({
82
- connectionString: `mysql://${config.mysql.user}:${config.mysql.password}@${config.mysql.host}:${config.mysql.port}/${database}?connectionLimit=3&waitForConnections=true`,
83
- bigIntMode: 'number',
84
- poolSize: 3,
85
- maxUses: 200,
86
- idleTimeoutMilliseconds: 30000,
87
- queueTimeoutMilliseconds: 60000,
88
- onError: (err) => {
89
- // Suppress "packets out of order" and inactivity warnings
90
- if (!err.message?.includes('packets out of order') &&
91
- !err.message?.includes('inactivity') &&
92
- !err.message?.includes('wait_timeout')) {
93
- console.error(`MySQL logger connection pool error: ${err.message}`);
94
- }
95
- },
96
- });
97
- // Create logs table if it doesn't exist
98
- await conn.query((0, mysql_1.sql) `
99
- CREATE TABLE IF NOT EXISTS \`logs\` (
100
- id INT AUTO_INCREMENT PRIMARY KEY,
101
- level VARCHAR(50),
102
- message TEXT,
103
- meta TEXT,
104
- stacktrace TEXT,
105
- timestamp DATETIME,
106
- INDEX idx_timestamp (timestamp),
107
- INDEX idx_level (level)
108
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
109
- `);
110
- // Run migrations: Add stacktrace column if it doesn't exist (for existing tables created before v2.2.0)
111
- try {
112
- const columns = await conn.query((0, mysql_1.sql) `SHOW COLUMNS FROM \`logs\` LIKE 'stacktrace'`);
113
- if (columns.length === 0) {
114
- console.log('[log-lib] Running migration: Adding stacktrace column...');
115
- await conn.query((0, mysql_1.sql) `ALTER TABLE \`logs\` ADD COLUMN \`stacktrace\` TEXT AFTER \`meta\``);
116
- console.log('[log-lib] Migration complete: stacktrace column added');
117
- }
118
- else {
119
- console.log('[log-lib] Migration check: stacktrace column already exists');
120
- }
121
- }
122
- catch (migrationErr) {
123
- console.error('[log-lib] Migration failed (non-critical):', migrationErr);
124
- // Don't fail initialization if migration fails
125
- }
126
- dbInitialized = true;
127
- console.log('[log-lib] Database initialization complete');
128
- }
129
- catch (err) {
130
- console.error('Failed to initialize logs database:', err);
131
- // 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;
132
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
+ };
133
95
  }
134
96
  function log(level, message, meta, fileLocation) {
135
97
  // Check if this log level should be filtered
@@ -315,26 +277,68 @@ function safeMeta(meta) {
315
277
  return {};
316
278
  return meta;
317
279
  }
318
- function storeInDB(level, message, meta, stacktrace) {
319
- if (!conn || !dbInitialized) {
320
- // Database not ready yet, skip DB logging
280
+ function storeInLoki(level, message, meta, stacktrace) {
281
+ if (!lokiConfig || !lokiConfig.enabled) {
321
282
  return;
322
283
  }
323
284
  try {
324
- const msg = safeToStringMessage(message);
325
- const metaObj = safeMeta(meta);
326
- const metaStr = (0, fast_safe_stringify_1.default)(metaObj).slice(0, 2000);
327
- const stackStr = stacktrace || '';
328
- // Fire and forget; avoid awaiting in hot path. Catch errors to avoid unhandled rejection.
329
- conn.query((0, mysql_1.sql) `INSERT INTO \`logs\` (level, message, meta, stacktrace, timestamp) VALUES (${level}, ${msg}, ${metaStr}, ${stackStr}, NOW())`).catch(e => {
330
- // fallback console output only - but don't spam
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;
312
+ }
313
+ if (lokiConfig.username && lokiConfig.password) {
314
+ const basic = Buffer.from(`${lokiConfig.username}:${lokiConfig.password}`).toString('base64');
315
+ headers['authorization'] = `Basic ${basic}`;
316
+ }
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) => {
331
331
  if (process.env.ENV_ID === 'dev') {
332
- console.error('Failed to persist log to DB', e);
332
+ console.error('[log-lib] Failed to persist log to Loki', e?.message || e);
333
333
  }
334
334
  });
335
+ req.write(body);
336
+ req.end();
335
337
  }
336
338
  catch (e) {
337
- console.error('Unexpected failure preparing log for DB', e);
339
+ if (process.env.ENV_ID === 'dev') {
340
+ console.error('[log-lib] Unexpected failure preparing log for Loki', e?.message || e);
341
+ }
338
342
  }
339
343
  }
340
344
  function createLogger(config = {}) {
@@ -344,11 +348,9 @@ function createLogger(config = {}) {
344
348
  process.env.LOG_LEVEL ||
345
349
  (process.env.ENV_ID === 'dev' ? 'debug' : 'info');
346
350
  currentLogLevel = LOG_LEVELS[configuredLevel] ?? LOG_LEVELS.info;
347
- // Start initialization asynchronously but don't wait for it (only if MySQL config provided)
348
- if (config.mysql) {
349
- initializeConnection(config).catch(err => {
350
- console.error('Error during log database initialization:', err);
351
- });
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.');
352
354
  }
353
355
  const logger = {
354
356
  info: (message, meta) => {
@@ -358,7 +360,7 @@ function createLogger(config = {}) {
358
360
  const frame = extractFirstProjectFrame(callStack);
359
361
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
360
362
  log('info', safeToStringMessage(message), metaObj, fileLocation);
361
- storeInDB('info', message, metaObj, fullTsStack);
363
+ storeInLoki('info', message, metaObj, fullTsStack);
362
364
  },
363
365
  error: (message, meta) => {
364
366
  const metaObj = safeMeta(meta);
@@ -375,9 +377,9 @@ function createLogger(config = {}) {
375
377
  if (causeChain.length) {
376
378
  console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
377
379
  }
378
- // For DB: include stack and error details in metadata
380
+ // For Loki: include stack and error details in metadata
379
381
  const enrichedMeta = { stack: message.stack, name: message.name, causeChain, ...metaObj };
380
- storeInDB('error', message.message, enrichedMeta, fullTsStack);
382
+ storeInLoki('error', message.message, enrichedMeta, fullTsStack);
381
383
  return;
382
384
  }
383
385
  const msgStr = safeToStringMessage(message);
@@ -387,7 +389,7 @@ function createLogger(config = {}) {
387
389
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
388
390
  log('error', msgStr, metaObj, fileLocation);
389
391
  printStackEnhanced(message);
390
- storeInDB('error', msgStr, metaObj, fullTsStack);
392
+ storeInLoki('error', msgStr, metaObj, fullTsStack);
391
393
  },
392
394
  errorEnriched: (message, error, meta) => {
393
395
  const metaObj = safeMeta(meta);
@@ -404,9 +406,9 @@ function createLogger(config = {}) {
404
406
  if (causeChain.length) {
405
407
  console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
406
408
  }
407
- // For DB: include stack and error details in metadata
409
+ // For Loki: include stack and error details in metadata
408
410
  const enrichedMeta = { stack: error.stack, name: error.name, causeChain, ...metaObj };
409
- storeInDB('error', `${message}: ${error.message}`, enrichedMeta, fullTsStack);
411
+ storeInLoki('error', `${message}: ${error.message}`, enrichedMeta, fullTsStack);
410
412
  return;
411
413
  }
412
414
  const errStr = safeToStringMessage(error);
@@ -416,7 +418,7 @@ function createLogger(config = {}) {
416
418
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
417
419
  log('error', `${message}: ${errStr}`, metaObj, fileLocation);
418
420
  printStackEnhanced(error);
419
- storeInDB('error', `${message}: ${errStr}`, metaObj, fullTsStack);
421
+ storeInLoki('error', `${message}: ${errStr}`, metaObj, fullTsStack);
420
422
  },
421
423
  warn: (message, meta) => {
422
424
  const metaObj = safeMeta(meta);
@@ -425,7 +427,7 @@ function createLogger(config = {}) {
425
427
  const frame = extractFirstProjectFrame(callStack);
426
428
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
427
429
  log('warn', safeToStringMessage(message), metaObj, fileLocation);
428
- storeInDB('warn', message, metaObj, fullTsStack);
430
+ storeInLoki('warn', message, metaObj, fullTsStack);
429
431
  },
430
432
  // do not store debug logs in DB
431
433
  debug: (message, meta) => {
@@ -443,7 +445,7 @@ function createLogger(config = {}) {
443
445
  const frame = extractFirstProjectFrame(callStack);
444
446
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
445
447
  log("info", messageString, undefined, fileLocation);
446
- // storeInDB("info", messageString); // Keep commented out as original
448
+ storeInLoki("info", messageString);
447
449
  },
448
450
  error: (msg, ...args) => {
449
451
  const errorMessage = (msg && msg.message) ? msg.message : String(msg);
@@ -453,8 +455,7 @@ function createLogger(config = {}) {
453
455
  const frame = extractFirstProjectFrame(callStack);
454
456
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
455
457
  log("error", errorMessage, meta, fileLocation);
456
- // Ensure string is passed to storeInDB
457
- 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);
458
459
  },
459
460
  warn: (msg, ...args) => {
460
461
  const messageString = typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : String(msg);
@@ -463,7 +464,7 @@ function createLogger(config = {}) {
463
464
  const frame = extractFirstProjectFrame(callStack);
464
465
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
465
466
  log("warn", messageString, undefined, fileLocation);
466
- storeInDB("warn", messageString, undefined, fullTsStack); // Pass stringified message
467
+ storeInLoki("warn", messageString, undefined, fullTsStack);
467
468
  },
468
469
  // do not store debug logs in DB
469
470
  debug: (msg, ...args) => {
@@ -479,7 +480,7 @@ function createLogger(config = {}) {
479
480
  const frame = extractFirstProjectFrame(callStack);
480
481
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
481
482
  log("error", messageString, undefined, fileLocation);
482
- storeInDB("error", messageString, undefined, fullTsStack);
483
+ storeInLoki("error", messageString, undefined, fullTsStack);
483
484
  // Exit after a brief delay to allow logs to flush
484
485
  setTimeout(() => process.exit(1), 100);
485
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.6",
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,12 +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;
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;
10
20
 
11
21
  const LOG_LEVELS: Record<LogLevel, number> = {
12
22
  debug: 0,
@@ -44,74 +54,37 @@ function cleanStackTrace(stack: string): string {
44
54
  }).join('\n');
45
55
  }
46
56
 
47
- async function initializeConnection(config: LoggerConfig) {
48
- if (dbInitialized || !config.mysql) return;
49
-
50
- try {
51
- const database = config.mysql.database || 'logs';
52
-
53
- // First connect without database to create it if needed
54
- const tempConn = createConnectionPool({
55
- connectionString: `mysql://${config.mysql.user}:${config.mysql.password}@${config.mysql.host}:${config.mysql.port}/?connectionLimit=1&waitForConnections=true`,
56
- bigIntMode: 'number',
57
- });
58
-
59
- await tempConn.query(sql`CREATE DATABASE IF NOT EXISTS ${sql.ident(database)} CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;`);
60
- await tempConn.dispose();
61
-
62
- // Now create the main connection pool with the logs database
63
- conn = createConnectionPool({
64
- connectionString: `mysql://${config.mysql.user}:${config.mysql.password}@${config.mysql.host}:${config.mysql.port}/${database}?connectionLimit=3&waitForConnections=true`,
65
- bigIntMode: 'number',
66
- poolSize: 3,
67
- maxUses: 200,
68
- idleTimeoutMilliseconds: 30_000,
69
- queueTimeoutMilliseconds: 60_000,
70
- onError: (err) => {
71
- // Suppress "packets out of order" and inactivity warnings
72
- if (!err.message?.includes('packets out of order') &&
73
- !err.message?.includes('inactivity') &&
74
- !err.message?.includes('wait_timeout')) {
75
- console.error(`MySQL logger connection pool error: ${err.message}`);
76
- }
77
- },
78
- });
79
-
80
- // Create logs table if it doesn't exist
81
- await conn.query(sql`
82
- CREATE TABLE IF NOT EXISTS \`logs\` (
83
- id INT AUTO_INCREMENT PRIMARY KEY,
84
- level VARCHAR(50),
85
- message TEXT,
86
- meta TEXT,
87
- stacktrace TEXT,
88
- timestamp DATETIME,
89
- INDEX idx_timestamp (timestamp),
90
- INDEX idx_level (level)
91
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
92
- `);
93
-
94
- // Run migrations: Add stacktrace column if it doesn't exist (for existing tables created before v2.2.0)
95
- try {
96
- const columns = await conn.query(sql`SHOW COLUMNS FROM \`logs\` LIKE 'stacktrace'`);
97
- if (columns.length === 0) {
98
- console.log('[log-lib] Running migration: Adding stacktrace column...');
99
- await conn.query(sql`ALTER TABLE \`logs\` ADD COLUMN \`stacktrace\` TEXT AFTER \`meta\``);
100
- console.log('[log-lib] Migration complete: stacktrace column added');
101
- } else {
102
- console.log('[log-lib] Migration check: stacktrace column already exists');
103
- }
104
- } catch (migrationErr) {
105
- console.error('[log-lib] Migration failed (non-critical):', migrationErr);
106
- // Don't fail initialization if migration fails
107
- }
108
-
109
- dbInitialized = true;
110
- console.log('[log-lib] Database initialization complete');
111
- } catch (err) {
112
- console.error('Failed to initialize logs database:', err);
113
- // 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;
114
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
+ };
115
88
  }
116
89
 
117
90
  function log(level: string, message: string, meta?: any, fileLocation?: string) {
@@ -296,25 +269,77 @@ function safeMeta(meta: any): any {
296
269
  return meta;
297
270
  }
298
271
 
299
- function storeInDB(level: string, message: any, meta?: any, stacktrace?: string) {
300
- if (!conn || !dbInitialized) {
301
- // Database not ready yet, skip DB logging
272
+ function storeInLoki(level: LogLevel, message: any, meta?: any, stacktrace?: string) {
273
+ if (!lokiConfig || !lokiConfig.enabled) {
302
274
  return;
303
275
  }
276
+
304
277
  try {
305
- const msg = safeToStringMessage(message);
306
- const metaObj = safeMeta(meta);
307
- const metaStr = jsonStringify(metaObj).slice(0, 2000);
308
- const stackStr = stacktrace || '';
309
- // Fire and forget; avoid awaiting in hot path. Catch errors to avoid unhandled rejection.
310
- conn.query(sql`INSERT INTO \`logs\` (level, message, meta, stacktrace, timestamp) VALUES (${level}, ${msg}, ${metaStr}, ${stackStr}, NOW())`).catch(e => {
311
- // fallback console output only - but don't spam
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;
308
+ }
309
+ if (lokiConfig.username && lokiConfig.password) {
310
+ const basic = Buffer.from(`${lokiConfig.username}:${lokiConfig.password}`).toString('base64');
311
+ headers['authorization'] = `Basic ${basic}`;
312
+ }
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) => {
312
332
  if (process.env.ENV_ID === 'dev') {
313
- console.error('Failed to persist log to DB', e);
333
+ console.error('[log-lib] Failed to persist log to Loki', e?.message || e);
314
334
  }
315
335
  });
316
- } catch (e) {
317
- console.error('Unexpected failure preparing log for DB', e);
336
+
337
+ req.write(body);
338
+ req.end();
339
+ } catch (e: any) {
340
+ if (process.env.ENV_ID === 'dev') {
341
+ console.error('[log-lib] Unexpected failure preparing log for Loki', e?.message || e);
342
+ }
318
343
  }
319
344
  }
320
345
 
@@ -327,11 +352,9 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
327
352
 
328
353
  currentLogLevel = LOG_LEVELS[configuredLevel] ?? LOG_LEVELS.info;
329
354
 
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
- });
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.');
335
358
  }
336
359
 
337
360
  const logger: Logger = {
@@ -343,7 +366,7 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
343
366
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
344
367
 
345
368
  log('info', safeToStringMessage(message), metaObj, fileLocation);
346
- storeInDB('info', message, metaObj, fullTsStack);
369
+ storeInLoki('info', message, metaObj, fullTsStack);
347
370
  },
348
371
  error: (message: string | Error | any, meta?: LogMetadata) => {
349
372
  const metaObj = safeMeta(meta);
@@ -362,9 +385,9 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
362
385
  console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
363
386
  }
364
387
 
365
- // For DB: include stack and error details in metadata
388
+ // For Loki: include stack and error details in metadata
366
389
  const enrichedMeta = {stack: message.stack, name: message.name, causeChain, ...metaObj};
367
- storeInDB('error', message.message, enrichedMeta, fullTsStack);
390
+ storeInLoki('error', message.message, enrichedMeta, fullTsStack);
368
391
  return;
369
392
  }
370
393
  const msgStr = safeToStringMessage(message);
@@ -375,7 +398,7 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
375
398
 
376
399
  log('error', msgStr, metaObj, fileLocation);
377
400
  printStackEnhanced(message);
378
- storeInDB('error', msgStr, metaObj, fullTsStack);
401
+ storeInLoki('error', msgStr, metaObj, fullTsStack);
379
402
  },
380
403
  errorEnriched: (message: string, error: Error | any, meta?: LogMetadata) => {
381
404
  const metaObj = safeMeta(meta);
@@ -394,9 +417,9 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
394
417
  console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
395
418
  }
396
419
 
397
- // For DB: include stack and error details in metadata
420
+ // For Loki: include stack and error details in metadata
398
421
  const enrichedMeta = {stack: error.stack, name: error.name, causeChain, ...metaObj};
399
- storeInDB('error', `${message}: ${error.message}`, enrichedMeta, fullTsStack);
422
+ storeInLoki('error', `${message}: ${error.message}`, enrichedMeta, fullTsStack);
400
423
  return;
401
424
  }
402
425
  const errStr = safeToStringMessage(error);
@@ -407,7 +430,7 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
407
430
 
408
431
  log('error', `${message}: ${errStr}`, metaObj, fileLocation);
409
432
  printStackEnhanced(error);
410
- storeInDB('error', `${message}: ${errStr}`, metaObj, fullTsStack);
433
+ storeInLoki('error', `${message}: ${errStr}`, metaObj, fullTsStack);
411
434
  },
412
435
  warn: (message: string, meta?: LogMetadata) => {
413
436
  const metaObj = safeMeta(meta);
@@ -417,7 +440,7 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
417
440
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
418
441
 
419
442
  log('warn', safeToStringMessage(message), metaObj, fileLocation);
420
- storeInDB('warn', message, metaObj, fullTsStack);
443
+ storeInLoki('warn', message, metaObj, fullTsStack);
421
444
  },
422
445
 
423
446
  // do not store debug logs in DB
@@ -439,7 +462,7 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
439
462
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
440
463
 
441
464
  log("info", messageString, undefined, fileLocation);
442
- // storeInDB("info", messageString); // Keep commented out as original
465
+ storeInLoki("info", messageString);
443
466
  },
444
467
  error: (msg: any, ...args: any[]) => {
445
468
  const errorMessage = (msg && msg.message) ? msg.message : String(msg);
@@ -450,8 +473,7 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
450
473
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
451
474
 
452
475
  log("error", errorMessage, meta, fileLocation);
453
- // Ensure string is passed to storeInDB
454
- storeInDB("error", typeof msg === 'object' ? jsonStringify(msg) : errorMessage, meta, fullTsStack);
476
+ storeInLoki("error", typeof msg === 'object' ? jsonStringify(msg) : errorMessage, meta, fullTsStack);
455
477
  },
456
478
  warn: (msg: any, ...args: any[]) => {
457
479
  const messageString = typeof msg === 'object' ? jsonStringify(msg) : String(msg);
@@ -461,7 +483,7 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
461
483
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
462
484
 
463
485
  log("warn", messageString, undefined, fileLocation);
464
- storeInDB("warn", messageString, undefined, fullTsStack); // Pass stringified message
486
+ storeInLoki("warn", messageString, undefined, fullTsStack);
465
487
  },
466
488
 
467
489
  // do not store debug logs in DB
@@ -481,7 +503,7 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
481
503
  const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
482
504
 
483
505
  log("error", messageString, undefined, fileLocation);
484
- storeInDB("error", messageString, undefined, fullTsStack);
506
+ storeInLoki("error", messageString, undefined, fullTsStack);
485
507
  // Exit after a brief delay to allow logs to flush
486
508
  setTimeout(() => process.exit(1), 100);
487
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;