@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 +46 -77
- package/dist/logger.js +99 -110
- package/dist/types.d.ts +14 -0
- package/example.ts +5 -7
- package/package.json +4 -5
- package/src/logger.ts +122 -117
- package/src/types.ts +15 -0
package/README.md
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
# log-lib
|
|
2
2
|
|
|
3
|
-
A TypeScript logging library with console output and
|
|
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
|
|
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
|
-
- **
|
|
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
|
-
- **
|
|
16
|
-
- **
|
|
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
|
|
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
|
-
##
|
|
30
|
+
## Loki Setup
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
By default the logger pushes to `http://loki:3100/loki/api/v1/push`.
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
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 +
|
|
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 +
|
|
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 +
|
|
152
|
+
Logs enriched error messages with context (console + Loki)
|
|
161
153
|
|
|
162
154
|
#### `logger.warn(message: string, meta?: LogMetadata)`
|
|
163
|
-
Logs warning messages (console +
|
|
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
|
-
###
|
|
173
|
+
### Loki Delivery
|
|
182
174
|
|
|
183
|
-
The logger
|
|
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
|
|
193
|
-
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
-
|
|
247
|
-
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
320
|
-
if (!
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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 (
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
332
|
+
console.error('[log-lib] Failed to persist log to Loki', e?.message || e);
|
|
344
333
|
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|
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
|
-
|
|
362
|
-
if (config.mysql) {
|
|
363
|
-
|
|
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
|
-
|
|
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
|
|
380
|
+
// For Loki: include stack and error details in metadata
|
|
391
381
|
const enrichedMeta = { stack: message.stack, name: message.name, causeChain, ...metaObj };
|
|
392
|
-
|
|
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
|
-
|
|
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
|
|
409
|
+
// For Loki: include stack and error details in metadata
|
|
420
410
|
const enrichedMeta = { stack: error.stack, name: error.name, causeChain, ...metaObj };
|
|
421
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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": "
|
|
4
|
-
"description": "Logging library with console and
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
301
|
-
if (!
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
|
333
|
+
console.error('[log-lib] Failed to persist log to Loki', e?.message || e);
|
|
328
334
|
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
|
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
|
-
|
|
350
|
-
if (config.mysql) {
|
|
351
|
-
|
|
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
|
-
|
|
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
|
|
388
|
+
// For Loki: include stack and error details in metadata
|
|
383
389
|
const enrichedMeta = {stack: message.stack, name: message.name, causeChain, ...metaObj};
|
|
384
|
-
|
|
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
|
-
|
|
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
|
|
420
|
+
// For Loki: include stack and error details in metadata
|
|
415
421
|
const enrichedMeta = {stack: error.stack, name: error.name, causeChain, ...metaObj};
|
|
416
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|