@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 +46 -77
- package/dist/logger.js +98 -97
- package/dist/types.d.ts +14 -0
- package/example.ts +5 -7
- package/package.json +4 -5
- package/src/logger.ts +123 -101
- 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,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
|
|
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;
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
319
|
-
if (!
|
|
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
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
348
|
-
if (config.mysql) {
|
|
349
|
-
|
|
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
|
-
|
|
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
|
|
380
|
+
// For Loki: include stack and error details in metadata
|
|
379
381
|
const enrichedMeta = { stack: message.stack, name: message.name, causeChain, ...metaObj };
|
|
380
|
-
|
|
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
|
-
|
|
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
|
|
409
|
+
// For Loki: include stack and error details in metadata
|
|
408
410
|
const enrichedMeta = { stack: error.stack, name: error.name, causeChain, ...metaObj };
|
|
409
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,12 +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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
300
|
-
if (!
|
|
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
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
|
333
|
+
console.error('[log-lib] Failed to persist log to Loki', e?.message || e);
|
|
314
334
|
}
|
|
315
335
|
});
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
331
|
-
if (config.mysql) {
|
|
332
|
-
|
|
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
|
-
|
|
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
|
|
388
|
+
// For Loki: include stack and error details in metadata
|
|
366
389
|
const enrichedMeta = {stack: message.stack, name: message.name, causeChain, ...metaObj};
|
|
367
|
-
|
|
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
|
-
|
|
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
|
|
420
|
+
// For Loki: include stack and error details in metadata
|
|
398
421
|
const enrichedMeta = {stack: error.stack, name: error.name, causeChain, ...metaObj};
|
|
399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|