metalog 3.1.17 → 4.0.0-prerelease
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 +335 -5
- package/metalog.d.ts +70 -43
- package/metalog.js +302 -220
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -14,21 +14,351 @@
|
|
|
14
14
|
## Usage
|
|
15
15
|
|
|
16
16
|
```js
|
|
17
|
-
const logger = await
|
|
17
|
+
const logger = await Logger.create({
|
|
18
18
|
path: './log', // absolute or relative path
|
|
19
19
|
workerId: 7, // mark for process or thread
|
|
20
|
-
|
|
21
|
-
writeBuffer: 64 * 1024, // buffer size (default 64kb)
|
|
22
|
-
keepDays: 5, // delete after N days, 0 - disable
|
|
20
|
+
flushInterval: 3000, // flush log to disk interval (default: 3s)
|
|
21
|
+
writeBuffer: 64 * 1024, // buffer size (default: 64kb)
|
|
22
|
+
keepDays: 5, // delete after N days, 0 - disable (default: 1)
|
|
23
23
|
home: process.cwd(), // remove substring from paths
|
|
24
|
-
json: false, // print logs in JSON format
|
|
24
|
+
json: false, // print logs in JSON format (default: false)
|
|
25
|
+
toFile: ['log', 'info', 'warn', 'error'], // tags to write to file (default: all)
|
|
26
|
+
toStdout: ['log', 'info', 'warn', 'error'], // tags to write to stdout (default: all)
|
|
27
|
+
createStream: () => fs.createWriteStream, // custom stream factory (optional)
|
|
28
|
+
crash: 'flush', // crash handling: 'flush' to flush buffer on exit (optional)
|
|
25
29
|
});
|
|
26
30
|
|
|
27
31
|
const { console } = logger;
|
|
32
|
+
|
|
28
33
|
console.log('Test message');
|
|
34
|
+
console.info('Info message');
|
|
35
|
+
console.warn('Warning message');
|
|
36
|
+
console.error('Error message');
|
|
37
|
+
console.debug('Debug message');
|
|
38
|
+
|
|
39
|
+
console.assert(true, 'Assertion passed');
|
|
40
|
+
console.assert(false, 'Assertion failed');
|
|
41
|
+
console.count('counter');
|
|
42
|
+
console.count('counter');
|
|
43
|
+
console.countReset('counter');
|
|
44
|
+
|
|
45
|
+
console.time('operation');
|
|
46
|
+
// ... some operation ...
|
|
47
|
+
console.timeEnd('operation');
|
|
48
|
+
console.timeLog('operation', 'Checkpoint');
|
|
49
|
+
|
|
50
|
+
console.group('Group 1');
|
|
51
|
+
console.log('Nested message');
|
|
52
|
+
console.groupCollapsed('Group 2');
|
|
53
|
+
console.log('Collapsed group message');
|
|
54
|
+
console.groupEnd();
|
|
55
|
+
console.groupEnd();
|
|
56
|
+
|
|
57
|
+
console.dir({ key: 'value' });
|
|
58
|
+
console.dirxml('<div>HTML content</div>');
|
|
59
|
+
console.table([
|
|
60
|
+
{ name: 'John', age: 30 },
|
|
61
|
+
{ name: 'Jane', age: 25 },
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
console.trace('Trace message');
|
|
65
|
+
|
|
29
66
|
await logger.close();
|
|
30
67
|
```
|
|
31
68
|
|
|
69
|
+
## Console API Compatibility
|
|
70
|
+
|
|
71
|
+
Metalog provides a fully compatible console implementation that supports all Node.js console methods:
|
|
72
|
+
|
|
73
|
+
- `console.log([data][, ...args])` - General logging
|
|
74
|
+
- `console.info([data][, ...args])` - Informational messages
|
|
75
|
+
- `console.warn([data][, ...args])` - Warning messages
|
|
76
|
+
- `console.error([data][, ...args])` - Error messages
|
|
77
|
+
- `console.debug([data][, ...args])` - Debug messages
|
|
78
|
+
- `console.assert(value[, ...message])` - Assertion testing
|
|
79
|
+
- `console.clear()` - Clear the console
|
|
80
|
+
- `console.count([label])` - Count occurrences
|
|
81
|
+
- `console.countReset([label])` - Reset counter
|
|
82
|
+
- `console.dir(obj[, options])` - Object inspection
|
|
83
|
+
- `console.dirxml(...data)` - XML/HTML inspection
|
|
84
|
+
- `console.group([...label])` - Start group
|
|
85
|
+
- `console.groupCollapsed()` - Start collapsed group
|
|
86
|
+
- `console.groupEnd()` - End group
|
|
87
|
+
- `console.table(tabularData[, properties])` - Table display
|
|
88
|
+
- `console.time([label])` - Start timer
|
|
89
|
+
- `console.timeEnd([label])` - End timer
|
|
90
|
+
- `console.timeLog([label][, ...data])` - Log timer value
|
|
91
|
+
- `console.trace([message][, ...args])` - Stack trace
|
|
92
|
+
|
|
93
|
+
All methods maintain the same behavior as Node.js native console, with output routed through the metalog system for consistent formatting and file logging.
|
|
94
|
+
|
|
95
|
+
## Configuration Options
|
|
96
|
+
|
|
97
|
+
### LoggerOptions
|
|
98
|
+
|
|
99
|
+
| Option | Type | Default | Description |
|
|
100
|
+
| --------------- | ---------- | ------------------------------------------- | --------------------------------------------------- |
|
|
101
|
+
| `path` | `string` | **required** | Directory path for log files (absolute or relative) |
|
|
102
|
+
| `home` | `string` | **required** | Base path to remove from stack traces |
|
|
103
|
+
| `workerId` | `number` | `undefined` | Worker/process identifier (appears as W0, W1, etc.) |
|
|
104
|
+
| `flushInterval` | `number` | `3000` | Flush buffer to disk interval in milliseconds |
|
|
105
|
+
| `writeBuffer` | `number` | `65536` | Buffer size threshold before flushing (64KB) |
|
|
106
|
+
| `keepDays` | `number` | `1` | Days to keep log files (0 = disable rotation) |
|
|
107
|
+
| `json` | `boolean` | `false` | Output logs in JSON format |
|
|
108
|
+
| `toFile` | `string[]` | `['log', 'info', 'warn', 'debug', 'error']` | Log tags to write to file |
|
|
109
|
+
| `toStdout` | `string[]` | `['log', 'info', 'warn', 'debug', 'error']` | Log tags to write to stdout |
|
|
110
|
+
| `createStream` | `function` | `fs.createWriteStream` | Custom stream factory function |
|
|
111
|
+
| `crash` | `string` | `undefined` | Crash handling mode ('flush' to flush on exit) |
|
|
112
|
+
|
|
113
|
+
### Log Tags
|
|
114
|
+
|
|
115
|
+
Metalog supports five log tags that can be filtered independently for file and console output:
|
|
116
|
+
|
|
117
|
+
- `log` - General logging
|
|
118
|
+
- `info` - Informational messages
|
|
119
|
+
- `warn` - Warning messages
|
|
120
|
+
- `debug` - Debug messages
|
|
121
|
+
- `error` - Error messages
|
|
122
|
+
|
|
123
|
+
## Advanced Usage
|
|
124
|
+
|
|
125
|
+
### Custom Stream Factory
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
const logger = await Logger.create({
|
|
129
|
+
path: './log',
|
|
130
|
+
home: process.cwd(),
|
|
131
|
+
createStream: (filePath) => {
|
|
132
|
+
// Custom compression stream
|
|
133
|
+
const fs = require('fs');
|
|
134
|
+
const zlib = require('zlib');
|
|
135
|
+
const gzip = zlib.createGzip();
|
|
136
|
+
const writeStream = fs.createWriteStream(filePath + '.gz');
|
|
137
|
+
return gzip.pipe(writeStream);
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Selective Logging
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
// Only log errors to file, all tags to console
|
|
146
|
+
const logger = await Logger.create({
|
|
147
|
+
path: './log',
|
|
148
|
+
home: process.cwd(),
|
|
149
|
+
toFile: ['error'],
|
|
150
|
+
toStdout: ['log', 'info', 'warn', 'debug', 'error'],
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Only log info and above tags to both file and console
|
|
154
|
+
const logger = await Logger.create({
|
|
155
|
+
path: './log',
|
|
156
|
+
home: process.cwd(),
|
|
157
|
+
toFile: ['info', 'warn', 'error'],
|
|
158
|
+
toStdout: ['info', 'warn', 'error'],
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### JSON Logging
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
const logger = await Logger.create({
|
|
166
|
+
path: './log',
|
|
167
|
+
home: process.cwd(),
|
|
168
|
+
json: true,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
logger.console.info('User action', { userId: 123, action: 'login' });
|
|
172
|
+
// Output: {"timestamp":"2025-01-07T10:30:00.000Z","worker":"W0","tag":"info","message":"User action","userId":123,"action":"login"}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Log Rotation and Cleanup
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
const logger = await Logger.create({
|
|
179
|
+
path: './log',
|
|
180
|
+
home: process.cwd(),
|
|
181
|
+
keepDays: 7, // Keep logs for 7 days
|
|
182
|
+
workerId: 1,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Manual rotation
|
|
186
|
+
await logger.rotate();
|
|
187
|
+
|
|
188
|
+
// Log files are automatically rotated daily
|
|
189
|
+
// Old files are cleaned up based on keepDays setting
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Error Handling
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
const logger = await Logger.create({
|
|
196
|
+
path: './log',
|
|
197
|
+
home: process.cwd(),
|
|
198
|
+
crash: 'flush', // Flush buffer on process exit
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
logger.on('error', (error) => {
|
|
202
|
+
console.error('Logger error:', error.message);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Graceful shutdown
|
|
206
|
+
process.on('SIGTERM', async () => {
|
|
207
|
+
await logger.close();
|
|
208
|
+
process.exit(0);
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## API Reference
|
|
213
|
+
|
|
214
|
+
### Logger Class
|
|
215
|
+
|
|
216
|
+
#### Constructor
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
new Logger(options: LoggerOptions): Promise<Logger>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### Static Methods
|
|
223
|
+
|
|
224
|
+
- `Logger.create(options: LoggerOptions): Promise<Logger>` - Create and open logger
|
|
225
|
+
|
|
226
|
+
#### Instance Methods
|
|
227
|
+
|
|
228
|
+
- `open(): Promise<Logger>` - Open log file and start logging
|
|
229
|
+
- `close(): Promise<void>` - Close logger and flush remaining data
|
|
230
|
+
- `rotate(): Promise<void>` - Manually trigger log rotation
|
|
231
|
+
- `write(tag: string, indent: number, args: unknown[]): void` - Low-level write method
|
|
232
|
+
- `flush(callback?: (error?: Error) => void): void` - Flush buffer to disk
|
|
233
|
+
|
|
234
|
+
#### Properties
|
|
235
|
+
|
|
236
|
+
- `active: boolean` - Whether logger is currently active
|
|
237
|
+
- `path: string` - Log directory path
|
|
238
|
+
- `home: string` - Home directory for path normalization
|
|
239
|
+
- `console: Console` - Console instance for logging
|
|
240
|
+
|
|
241
|
+
### BufferedStream Class
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
const stream = new BufferedStream({
|
|
245
|
+
stream: fs.createWriteStream('output.log'),
|
|
246
|
+
writeBuffer: 32 * 1024, // 32KB buffer
|
|
247
|
+
flushInterval: 5000, // 5 second flush interval
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
stream.write(Buffer.from('data'));
|
|
251
|
+
stream.flush();
|
|
252
|
+
await stream.close();
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Formatter Class
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
const formatter = new Formatter({
|
|
259
|
+
worker: 'W1',
|
|
260
|
+
home: '/app',
|
|
261
|
+
json: false,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const formatted = formatter.formatPretty('info', 0, ['Message']);
|
|
265
|
+
const jsonOutput = formatter.formatJson('error', 0, [error]);
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Best Practices
|
|
269
|
+
|
|
270
|
+
### Performance Optimization
|
|
271
|
+
|
|
272
|
+
1. **Buffer Size**: Adjust `writeBuffer` based on your log volume
|
|
273
|
+
- High volume: 128KB or larger
|
|
274
|
+
- Low volume: 16KB or smaller
|
|
275
|
+
|
|
276
|
+
2. **Flush Interval**: Balance between performance and data safety
|
|
277
|
+
- Production: 3-10 seconds
|
|
278
|
+
- Development: 1-3 seconds
|
|
279
|
+
|
|
280
|
+
3. **Selective Logging**: Use `toFile` and `toStdout` to reduce I/O
|
|
281
|
+
```js
|
|
282
|
+
// Production: Only errors to file, warnings+ to console
|
|
283
|
+
toFile: ['error'],
|
|
284
|
+
toStdout: ['warn', 'error']
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Log Management
|
|
288
|
+
|
|
289
|
+
1. **Rotation Strategy**: Set appropriate `keepDays` based on storage
|
|
290
|
+
2. **Path Organization**: Use structured paths for multi-service deployments
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
path: `/var/log/app/${process.env.NODE_ENV}/${process.env.SERVICE_NAME}`;
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
3. **Error Handling**: Always handle logger errors
|
|
297
|
+
```js
|
|
298
|
+
logger.on('error', (error) => {
|
|
299
|
+
// Fallback logging or alerting
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Development vs Production
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
307
|
+
|
|
308
|
+
const logger = await Logger.create({
|
|
309
|
+
path: './log',
|
|
310
|
+
home: process.cwd(),
|
|
311
|
+
json: !isDevelopment, // JSON in production, pretty in dev
|
|
312
|
+
toFile: isDevelopment ? ['log', 'info', 'warn', 'error'] : ['error'],
|
|
313
|
+
toStdout: isDevelopment
|
|
314
|
+
? ['log', 'info', 'warn', 'debug', 'error']
|
|
315
|
+
: ['warn', 'error'],
|
|
316
|
+
flushInterval: isDevelopment ? 1000 : 5000,
|
|
317
|
+
keepDays: isDevelopment ? 1 : 30,
|
|
318
|
+
});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Troubleshooting
|
|
322
|
+
|
|
323
|
+
### Common Issues
|
|
324
|
+
|
|
325
|
+
**Logs not appearing in files:**
|
|
326
|
+
|
|
327
|
+
- Check `path` directory exists and is writable
|
|
328
|
+
- Verify `toFile` array includes desired log tags
|
|
329
|
+
- Ensure logger is properly opened with `await logger.open()`
|
|
330
|
+
|
|
331
|
+
**High memory usage:**
|
|
332
|
+
|
|
333
|
+
- Reduce `writeBuffer` size
|
|
334
|
+
- Increase `flushInterval` frequency
|
|
335
|
+
- Use selective logging with `toFile`/`toStdout`
|
|
336
|
+
|
|
337
|
+
**Missing logs on crash:**
|
|
338
|
+
|
|
339
|
+
- Set `crash: 'flush'` option
|
|
340
|
+
- Handle process signals properly
|
|
341
|
+
- Use try/catch around critical operations
|
|
342
|
+
|
|
343
|
+
**Performance issues:**
|
|
344
|
+
|
|
345
|
+
- Use JSON format for high-volume logging
|
|
346
|
+
- Disable file logging for debug tags in production
|
|
347
|
+
- Consider using separate loggers for different components
|
|
348
|
+
|
|
349
|
+
### Debug Mode
|
|
350
|
+
|
|
351
|
+
```js
|
|
352
|
+
// Enable debug logging
|
|
353
|
+
const logger = await Logger.create({
|
|
354
|
+
path: './log',
|
|
355
|
+
home: process.cwd(),
|
|
356
|
+
toStdout: ['debug', 'info', 'warn', 'error'],
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
logger.console.debug('Debug information', { data: 'value' });
|
|
360
|
+
```
|
|
361
|
+
|
|
32
362
|
## License & Contributors
|
|
33
363
|
|
|
34
364
|
Copyright (c) 2017-2025 [Metarhia contributors](https://github.com/metarhia/metalog/graphs/contributors).
|
package/metalog.d.ts
CHANGED
|
@@ -5,70 +5,97 @@ interface LoggerOptions {
|
|
|
5
5
|
home: string;
|
|
6
6
|
workerId?: number;
|
|
7
7
|
createStream?: () => NodeJS.WritableStream;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
keepDays
|
|
8
|
+
writeBuffer?: number;
|
|
9
|
+
flushInterval?: number;
|
|
10
|
+
keepDays?: number;
|
|
11
11
|
json?: boolean;
|
|
12
12
|
toFile?: Array<string>;
|
|
13
13
|
toStdout?: Array<string>;
|
|
14
14
|
crash?: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
interface
|
|
18
|
-
|
|
17
|
+
interface BufferedStreamOptions {
|
|
18
|
+
stream?: NodeJS.WritableStream;
|
|
19
|
+
writeBuffer?: number;
|
|
20
|
+
flushInterval?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface FormatterOptions {
|
|
24
|
+
json?: boolean;
|
|
25
|
+
worker?: string;
|
|
26
|
+
home?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class BufferedStream extends EventEmitter {
|
|
30
|
+
constructor(options?: BufferedStreamOptions);
|
|
31
|
+
write(buffer: Buffer): void;
|
|
32
|
+
flush(callback?: (error?: Error) => void): void;
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class Formatter {
|
|
37
|
+
constructor(options?: FormatterOptions);
|
|
38
|
+
format(tag: string, indent: number, args: unknown[]): string;
|
|
39
|
+
formatPretty(tag: string, indent: number, args: unknown[]): string;
|
|
40
|
+
formatFile(tag: string, indent: number, args: unknown[]): string;
|
|
41
|
+
formatJson(tag: string, indent: number, args: unknown[]): string;
|
|
42
|
+
normalizeStack(stack: string): string;
|
|
43
|
+
expandError(error: Error): unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class Console {
|
|
47
|
+
constructor(logger: Logger);
|
|
48
|
+
assert(value: unknown, ...message: unknown[]): void;
|
|
19
49
|
clear(): void;
|
|
20
50
|
count(label?: string): void;
|
|
21
51
|
countReset(label?: string): void;
|
|
22
|
-
debug(...args: unknown[]): void;
|
|
23
|
-
dir(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
error(...args: unknown[]): void;
|
|
29
|
-
group(...args: unknown[]): void;
|
|
30
|
-
groupCollapsed(...args: unknown[]): void;
|
|
52
|
+
debug(data: unknown, ...args: unknown[]): void;
|
|
53
|
+
dir(obj: unknown, options?: unknown): void;
|
|
54
|
+
dirxml(...data: unknown[]): void;
|
|
55
|
+
error(data?: unknown, ...args: unknown[]): void;
|
|
56
|
+
group(...label: unknown[]): void;
|
|
57
|
+
groupCollapsed(...label: unknown[]): void;
|
|
31
58
|
groupEnd(): void;
|
|
32
|
-
|
|
59
|
+
info(data?: unknown, ...args: unknown[]): void;
|
|
60
|
+
log(data?: unknown, ...args: unknown[]): void;
|
|
61
|
+
table(tabularData: unknown, properties?: string[]): void;
|
|
33
62
|
time(label?: string): void;
|
|
34
63
|
timeEnd(label?: string): void;
|
|
35
|
-
timeLog(label
|
|
64
|
+
timeLog(label?: string, ...data: unknown[]): void;
|
|
65
|
+
trace(message?: unknown, ...args: unknown[]): void;
|
|
66
|
+
warn(data?: unknown, ...args: unknown[]): void;
|
|
36
67
|
}
|
|
37
68
|
|
|
38
69
|
export class Logger extends EventEmitter {
|
|
39
70
|
active: boolean;
|
|
40
71
|
path: string;
|
|
41
|
-
workerId: string;
|
|
42
|
-
createStream: () => NodeJS.WritableStream;
|
|
43
|
-
writeInterval: number;
|
|
44
|
-
writeBuffer: number;
|
|
45
|
-
keepDays: number;
|
|
46
72
|
home: string;
|
|
47
|
-
json: boolean;
|
|
48
|
-
stream: NodeJS.WritableStream;
|
|
49
|
-
reopenTimer: NodeJS.Timer;
|
|
50
|
-
flushTimer: NodeJS.Timer;
|
|
51
|
-
lock: boolean;
|
|
52
|
-
buffer: Array<Buffer>;
|
|
53
|
-
bufferLength: number;
|
|
54
|
-
file: string;
|
|
55
|
-
toFile: Record<string, boolean>;
|
|
56
|
-
fsEnabled: boolean;
|
|
57
|
-
toStdout: Record<string, boolean>;
|
|
58
73
|
console: Console;
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
|
|
75
|
+
constructor(options: LoggerOptions);
|
|
76
|
+
static create(options: LoggerOptions): Promise<Logger>;
|
|
61
77
|
open(): Promise<Logger>;
|
|
62
78
|
close(): Promise<void>;
|
|
63
79
|
rotate(): Promise<void>;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
write(tag: string, indent: number, args: unknown[]): void;
|
|
81
|
+
flush(callback?: (error?: Error) => void): void;
|
|
82
|
+
|
|
83
|
+
#options: LoggerOptions;
|
|
84
|
+
#worker: string;
|
|
85
|
+
#createStream: () => NodeJS.WritableStream;
|
|
86
|
+
#keepDays: number;
|
|
87
|
+
#stream: NodeJS.WritableStream | null;
|
|
88
|
+
#rotationTimer: NodeJS.Timer | null;
|
|
89
|
+
#file: string;
|
|
90
|
+
#fsEnabled: boolean;
|
|
91
|
+
#toFile: Record<string, boolean> | null;
|
|
92
|
+
#toStdout: Record<string, boolean> | null;
|
|
93
|
+
#buffer: BufferedStream | null;
|
|
94
|
+
#formatter: Formatter;
|
|
95
|
+
|
|
96
|
+
#createDir(): Promise<void>;
|
|
97
|
+
#setupCrashHandling(): void;
|
|
72
98
|
}
|
|
73
99
|
|
|
74
|
-
export function
|
|
100
|
+
export function nowDays(): number;
|
|
101
|
+
export function nameToDays(fileName: string): number;
|
package/metalog.js
CHANGED
|
@@ -4,26 +4,26 @@ const fs = require('node:fs');
|
|
|
4
4
|
const fsp = fs.promises;
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const util = require('node:util');
|
|
7
|
-
const
|
|
7
|
+
const EventEmitter = require('node:events');
|
|
8
8
|
const readline = require('node:readline');
|
|
9
9
|
const metautil = require('metautil');
|
|
10
10
|
const concolor = require('concolor');
|
|
11
11
|
|
|
12
12
|
const DAY_MILLISECONDS = metautil.duration('1d');
|
|
13
|
-
const
|
|
14
|
-
const
|
|
13
|
+
const DEFAULT_FLUSH_INTERVAL = metautil.duration('3s');
|
|
14
|
+
const DEFAULT_BUFFER_THRESHOLD = 64 * 1024;
|
|
15
15
|
const DEFAULT_KEEP_DAYS = 1;
|
|
16
16
|
const STACK_AT = ' at ';
|
|
17
|
-
const
|
|
17
|
+
const TAG_LENGTH = 6;
|
|
18
18
|
const LINE_SEPARATOR = ';';
|
|
19
19
|
const INDENT = 2;
|
|
20
20
|
const DATE_LEN = 'YYYY-MM-DD'.length;
|
|
21
21
|
const TIME_START = DATE_LEN + 1;
|
|
22
22
|
const TIME_END = TIME_START + 'HH:MM:SS'.length;
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const LOG_TAGS = ['log', 'info', 'warn', 'debug', 'error'];
|
|
25
25
|
|
|
26
|
-
const
|
|
26
|
+
const TAG_COLOR = concolor({
|
|
27
27
|
log: 'b,black/white',
|
|
28
28
|
info: 'b,white/blue',
|
|
29
29
|
warn: 'b,black/yellow',
|
|
@@ -47,11 +47,9 @@ const DEFAULT_FLAGS = {
|
|
|
47
47
|
error: false,
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
const
|
|
50
|
+
const logTags = (tags) => {
|
|
51
51
|
const flags = { ...DEFAULT_FLAGS };
|
|
52
|
-
for (const
|
|
53
|
-
flags[type] = true;
|
|
54
|
-
}
|
|
52
|
+
for (const tag of tags) flags[tag] = true;
|
|
55
53
|
return flags;
|
|
56
54
|
};
|
|
57
55
|
|
|
@@ -64,9 +62,17 @@ const nowDays = () => {
|
|
|
64
62
|
return Math.floor(date.getTime() / DAY_MILLISECONDS);
|
|
65
63
|
};
|
|
66
64
|
|
|
67
|
-
const nameToDays = (fileName) => {
|
|
65
|
+
const nameToDays = (fileName = '') => {
|
|
66
|
+
if (fileName.length < DATE_LEN) {
|
|
67
|
+
throw new Error(`Invalid filename: ${fileName}`);
|
|
68
|
+
}
|
|
68
69
|
const date = fileName.substring(0, DATE_LEN);
|
|
69
|
-
const
|
|
70
|
+
const [year, month, day] = date.split('-').map(Number);
|
|
71
|
+
const fileDate = new Date(year, month - 1, day, 0, 0, 0, 0);
|
|
72
|
+
const fileTime = fileDate.getTime();
|
|
73
|
+
if (isNaN(fileTime)) {
|
|
74
|
+
throw new Error(`Invalid filename: ${fileName}`);
|
|
75
|
+
}
|
|
70
76
|
return Math.floor(fileTime / DAY_MILLISECONDS);
|
|
71
77
|
};
|
|
72
78
|
|
|
@@ -77,22 +83,148 @@ const getNextReopen = () => {
|
|
|
77
83
|
return nextDate - curTime + DAY_MILLISECONDS;
|
|
78
84
|
};
|
|
79
85
|
|
|
86
|
+
class BufferedStream extends EventEmitter {
|
|
87
|
+
#writable = null;
|
|
88
|
+
#buffers = [];
|
|
89
|
+
#size = 0;
|
|
90
|
+
#threshold = DEFAULT_BUFFER_THRESHOLD;
|
|
91
|
+
#flushing = false;
|
|
92
|
+
#flushTimer = null;
|
|
93
|
+
|
|
94
|
+
constructor(options = {}) {
|
|
95
|
+
super();
|
|
96
|
+
const { stream, writeBuffer, flushInterval } = options;
|
|
97
|
+
if (!stream) throw new Error('Stream is required');
|
|
98
|
+
this.#writable = stream;
|
|
99
|
+
if (writeBuffer) this.#threshold = writeBuffer;
|
|
100
|
+
const interval = flushInterval || DEFAULT_FLUSH_INTERVAL;
|
|
101
|
+
this.#flushTimer = setInterval(() => void this.flush(), interval);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
write(buffer) {
|
|
105
|
+
this.#buffers.push(buffer);
|
|
106
|
+
this.#size += buffer.length;
|
|
107
|
+
if (this.#size >= this.#threshold) this.flush();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
flush(callback) {
|
|
111
|
+
if (this.#flushing) {
|
|
112
|
+
if (callback) this.once('drain', callback);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (this.#size === 0) {
|
|
116
|
+
if (callback) callback();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (this.#writable.destroyed || this.#writable.closed) {
|
|
120
|
+
if (callback) callback();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
this.#flushing = true;
|
|
124
|
+
const buffer = Buffer.concat(this.#buffers);
|
|
125
|
+
this.#buffers.length = 0;
|
|
126
|
+
this.#size = 0;
|
|
127
|
+
this.#writable.write(buffer, (error) => {
|
|
128
|
+
this.#flushing = false;
|
|
129
|
+
this.emit('drain');
|
|
130
|
+
if (callback) callback(error);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async close() {
|
|
135
|
+
clearInterval(this.#flushTimer);
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
this.flush((error) => {
|
|
138
|
+
if (error) return void reject(error);
|
|
139
|
+
this.#writable.end(resolve);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
class Formatter {
|
|
146
|
+
#worker = 'W0';
|
|
147
|
+
#home = './';
|
|
148
|
+
|
|
149
|
+
constructor(options = {}) {
|
|
150
|
+
const { worker, home } = options;
|
|
151
|
+
if (worker) this.#worker = worker;
|
|
152
|
+
if (home) this.#home = home;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
format(tag, indent, args) {
|
|
156
|
+
let line = util.format(...args);
|
|
157
|
+
if (tag === 'error' || tag === 'debug') line = this.normalizeStack(line);
|
|
158
|
+
return ' '.repeat(indent) + line;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
formatPretty(tag, indent, args) {
|
|
162
|
+
const dateTime = new Date().toISOString();
|
|
163
|
+
const message = this.format(tag, indent, args);
|
|
164
|
+
const normalColor = TEXT_COLOR[tag];
|
|
165
|
+
const markColor = TAG_COLOR[tag];
|
|
166
|
+
const time = normalColor(dateTime.substring(TIME_START, TIME_END));
|
|
167
|
+
const id = normalColor(this.#worker);
|
|
168
|
+
const mark = markColor(' ' + tag.padEnd(TAG_LENGTH));
|
|
169
|
+
const msg = normalColor(message);
|
|
170
|
+
return `${time} ${id} ${mark} ${msg}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
formatFile(tag, indent, args) {
|
|
174
|
+
const dateTime = new Date().toISOString();
|
|
175
|
+
const message = this.format(tag, indent, args);
|
|
176
|
+
const msg = metautil.replace(message, '\n', LINE_SEPARATOR);
|
|
177
|
+
return `${dateTime} [${tag}] ${msg}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
formatJson(tag, indent, args) {
|
|
181
|
+
const timestamp = new Date().toISOString();
|
|
182
|
+
const json = { timestamp, worker: this.#worker, tag, message: null };
|
|
183
|
+
let data = args.slice();
|
|
184
|
+
const head = data[0];
|
|
185
|
+
if (metautil.isError(head)) {
|
|
186
|
+
json.error = this.expandError(head);
|
|
187
|
+
data = data.slice(1);
|
|
188
|
+
} else if (typeof head === 'object') {
|
|
189
|
+
Object.assign(json, head);
|
|
190
|
+
data = data.slice(1);
|
|
191
|
+
}
|
|
192
|
+
json.message = util.format(...data);
|
|
193
|
+
return JSON.stringify(json);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
normalizeStack(stack) {
|
|
197
|
+
if (!stack) return 'No stack trace to log';
|
|
198
|
+
let res = metautil.replace(stack, STACK_AT, '');
|
|
199
|
+
if (this.#home) res = metautil.replace(res, this.#home, '');
|
|
200
|
+
return res;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
expandError(error) {
|
|
204
|
+
return {
|
|
205
|
+
message: error.message,
|
|
206
|
+
stack: this.normalizeStack(error.stack),
|
|
207
|
+
...error,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
80
212
|
class Console {
|
|
81
|
-
#
|
|
213
|
+
#logger;
|
|
82
214
|
#groupIndent = 0;
|
|
83
215
|
#counts = new Map();
|
|
84
216
|
#times = new Map();
|
|
85
217
|
#readline = readline;
|
|
86
218
|
|
|
87
|
-
constructor(
|
|
88
|
-
this.#
|
|
219
|
+
constructor(logger) {
|
|
220
|
+
this.#logger = logger;
|
|
89
221
|
}
|
|
90
222
|
|
|
91
223
|
assert(assertion, ...args) {
|
|
92
224
|
if (!assertion) {
|
|
93
225
|
const noArgs = args.length === 0;
|
|
94
226
|
const message = noArgs ? 'Assertion failed' : util.format(...args);
|
|
95
|
-
this.#write('error', this.#groupIndent, message);
|
|
227
|
+
this.#logger.write('error', this.#groupIndent, [message]);
|
|
96
228
|
}
|
|
97
229
|
}
|
|
98
230
|
|
|
@@ -105,7 +237,7 @@ class Console {
|
|
|
105
237
|
let cnt = this.#counts.get(label) || 0;
|
|
106
238
|
cnt++;
|
|
107
239
|
this.#counts.set(label, cnt);
|
|
108
|
-
this.#write('debug', this.#groupIndent, `${label}: ${cnt}`);
|
|
240
|
+
this.#logger.write('debug', this.#groupIndent, [`${label}: ${cnt}`]);
|
|
109
241
|
}
|
|
110
242
|
|
|
111
243
|
countReset(label = 'default') {
|
|
@@ -113,33 +245,38 @@ class Console {
|
|
|
113
245
|
}
|
|
114
246
|
|
|
115
247
|
debug(...args) {
|
|
116
|
-
this.#write('debug', this.#groupIndent,
|
|
248
|
+
this.#logger.write('debug', this.#groupIndent, args);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
dir(obj, options) {
|
|
252
|
+
const inspected = util.inspect(obj, options);
|
|
253
|
+
this.#logger.write('debug', this.#groupIndent, [inspected]);
|
|
117
254
|
}
|
|
118
255
|
|
|
119
|
-
|
|
120
|
-
this.#write('debug', this.#groupIndent,
|
|
256
|
+
dirxml(...data) {
|
|
257
|
+
this.#logger.write('debug', this.#groupIndent, data);
|
|
121
258
|
}
|
|
122
259
|
|
|
123
260
|
trace(...args) {
|
|
124
261
|
const msg = util.format(...args);
|
|
125
262
|
const err = new Error(msg);
|
|
126
|
-
this.#write('debug', this.#groupIndent, `Trace${err.stack}`);
|
|
263
|
+
this.#logger.write('debug', this.#groupIndent, [`Trace${err.stack}`]);
|
|
127
264
|
}
|
|
128
265
|
|
|
129
266
|
info(...args) {
|
|
130
|
-
this.#write('info', this.#groupIndent,
|
|
267
|
+
this.#logger.write('info', this.#groupIndent, args);
|
|
131
268
|
}
|
|
132
269
|
|
|
133
270
|
log(...args) {
|
|
134
|
-
this.#write('log', this.#groupIndent,
|
|
271
|
+
this.#logger.write('log', this.#groupIndent, args);
|
|
135
272
|
}
|
|
136
273
|
|
|
137
274
|
warn(...args) {
|
|
138
|
-
this.#write('warn', this.#groupIndent,
|
|
275
|
+
this.#logger.write('warn', this.#groupIndent, args);
|
|
139
276
|
}
|
|
140
277
|
|
|
141
278
|
error(...args) {
|
|
142
|
-
this.#write('error', this.#groupIndent,
|
|
279
|
+
this.#logger.write('error', this.#groupIndent, args);
|
|
143
280
|
}
|
|
144
281
|
|
|
145
282
|
group(...args) {
|
|
@@ -156,8 +293,22 @@ class Console {
|
|
|
156
293
|
this.#groupIndent -= INDENT;
|
|
157
294
|
}
|
|
158
295
|
|
|
159
|
-
table(tabularData) {
|
|
160
|
-
|
|
296
|
+
table(tabularData, properties) {
|
|
297
|
+
const opts = { showHidden: false, depth: null, colors: false };
|
|
298
|
+
let data = tabularData;
|
|
299
|
+
if (properties) {
|
|
300
|
+
if (!Array.isArray(data)) data = [data];
|
|
301
|
+
data = data.map((item) => {
|
|
302
|
+
const record = {};
|
|
303
|
+
for (const prop of properties) {
|
|
304
|
+
if (Object.prototype.hasOwnProperty.call(item, prop)) {
|
|
305
|
+
record[prop] = item[prop];
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return record;
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
this.#logger.write('log', 0, [util.inspect(data, opts)]);
|
|
161
312
|
}
|
|
162
313
|
|
|
163
314
|
time(label = 'default') {
|
|
@@ -172,131 +323,124 @@ class Console {
|
|
|
172
323
|
this.#times.delete(label);
|
|
173
324
|
}
|
|
174
325
|
|
|
175
|
-
timeLog(label, ...
|
|
326
|
+
timeLog(label = 'default', ...data) {
|
|
176
327
|
const startTime = this.#times.get(label);
|
|
177
328
|
if (startTime === undefined) {
|
|
178
329
|
const msg = `Warning: No such label '${label}'`;
|
|
179
|
-
this.#write('warn', this.#groupIndent, msg);
|
|
330
|
+
this.#logger.write('warn', this.#groupIndent, [msg]);
|
|
180
331
|
return;
|
|
181
332
|
}
|
|
182
|
-
|
|
333
|
+
const totalTime = process.hrtime(startTime);
|
|
334
|
+
const totalTimeMs = totalTime[0] * 1e3 + totalTime[1] / 1e6;
|
|
335
|
+
const message = data.length > 0 ? util.format(...data) : '';
|
|
336
|
+
const output = `${label}: ${totalTimeMs}ms${message ? ' ' + message : ''}`;
|
|
337
|
+
this.#logger.write('debug', this.#groupIndent, [output]);
|
|
183
338
|
}
|
|
184
339
|
}
|
|
185
340
|
|
|
186
|
-
class Logger extends
|
|
341
|
+
class Logger extends EventEmitter {
|
|
342
|
+
active = false;
|
|
343
|
+
#worker = 'W0';
|
|
344
|
+
#createStream = fs.createWriteStream;
|
|
345
|
+
#options = null;
|
|
346
|
+
#keepDays = DEFAULT_KEEP_DAYS;
|
|
347
|
+
#stream = null;
|
|
348
|
+
#rotationTimer = null;
|
|
349
|
+
#file = '';
|
|
350
|
+
#fsEnabled = false;
|
|
351
|
+
#toFile = null;
|
|
352
|
+
#toStdout = null;
|
|
353
|
+
#buffer = null;
|
|
354
|
+
#formatter = null;
|
|
355
|
+
|
|
187
356
|
constructor(options) {
|
|
188
357
|
super();
|
|
189
|
-
|
|
190
|
-
const {
|
|
191
|
-
const { toFile =
|
|
192
|
-
this.active = false;
|
|
358
|
+
this.#options = options;
|
|
359
|
+
const { workerId, createStream, keepDays, home, crash, json } = options;
|
|
360
|
+
const { toFile = LOG_TAGS, toStdout = LOG_TAGS } = options;
|
|
193
361
|
this.path = options.path;
|
|
194
|
-
this.workerId = `W${workerId}`;
|
|
195
|
-
this.createStream = createStream;
|
|
196
|
-
this.writeInterval = writeInterval || DEFAULT_WRITE_INTERVAL;
|
|
197
|
-
this.writeBuffer = writeBuffer || DEFAULT_BUFFER_SIZE;
|
|
198
|
-
this.keepDays = keepDays || DEFAULT_KEEP_DAYS;
|
|
199
362
|
this.home = home;
|
|
200
|
-
this.
|
|
201
|
-
this
|
|
202
|
-
this
|
|
203
|
-
this
|
|
204
|
-
this
|
|
205
|
-
this
|
|
206
|
-
this.bufferLength = 0;
|
|
207
|
-
this.file = '';
|
|
208
|
-
this.toFile = logTypes(toFile);
|
|
209
|
-
this.fsEnabled = toFile.length !== 0;
|
|
210
|
-
this.toStdout = logTypes(toStdout);
|
|
211
|
-
this.console = new Console((...args) => this.write(...args));
|
|
363
|
+
this.console = new Console(this);
|
|
364
|
+
if (workerId) this.#worker = `W${workerId}`;
|
|
365
|
+
if (toFile) this.#toFile = logTags(toFile);
|
|
366
|
+
if (toStdout) this.#toStdout = logTags(toStdout);
|
|
367
|
+
if (createStream) this.#createStream = createStream;
|
|
368
|
+
if (keepDays) this.#keepDays = keepDays;
|
|
212
369
|
if (crash === 'flush') this.#setupCrashHandling();
|
|
370
|
+
this.#fsEnabled = toFile.length !== 0;
|
|
371
|
+
this.#buffer = null;
|
|
372
|
+
this.#formatter = new Formatter({ json, worker: this.#worker, home });
|
|
213
373
|
return this.open();
|
|
214
374
|
}
|
|
215
375
|
|
|
216
|
-
|
|
217
|
-
return new
|
|
218
|
-
fs.access(this.path, (err) => {
|
|
219
|
-
if (!err) resolve();
|
|
220
|
-
fs.mkdir(this.path, (err) => {
|
|
221
|
-
if (!err || err.code === 'EEXIST') return void resolve();
|
|
222
|
-
const error = new Error(`Can not create directory: ${this.path}\n`);
|
|
223
|
-
this.emit('error', error);
|
|
224
|
-
reject();
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
});
|
|
376
|
+
static async create(options) {
|
|
377
|
+
return new Logger(options);
|
|
228
378
|
}
|
|
229
379
|
|
|
230
380
|
async open() {
|
|
231
381
|
if (this.active) return this;
|
|
232
382
|
this.active = true;
|
|
233
|
-
if (!this
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
await this.createLogDir();
|
|
238
|
-
const fileName = metautil.nowDate() + '-' + this.workerId + '.log';
|
|
239
|
-
this.file = path.join(this.path, fileName);
|
|
383
|
+
if (!this.#fsEnabled) return this;
|
|
384
|
+
await this.#createDir();
|
|
385
|
+
const fileName = metautil.nowDate() + '-' + this.#worker + '.log';
|
|
386
|
+
this.#file = path.join(this.path, fileName);
|
|
240
387
|
const nextReopen = getNextReopen();
|
|
241
|
-
this
|
|
388
|
+
this.#rotationTimer = setTimeout(() => {
|
|
242
389
|
this.once('close', () => {
|
|
243
390
|
this.open();
|
|
244
391
|
});
|
|
245
|
-
this.close().catch((
|
|
246
|
-
|
|
247
|
-
this.emit('error', err);
|
|
392
|
+
this.close().catch((error) => {
|
|
393
|
+
this.emit('error', error);
|
|
248
394
|
});
|
|
249
395
|
}, nextReopen);
|
|
250
|
-
if (this
|
|
251
|
-
|
|
252
|
-
this
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
this.
|
|
396
|
+
if (this.#keepDays) await this.rotate();
|
|
397
|
+
const stream = this.#createStream(this.#file, { flags: 'a' });
|
|
398
|
+
this.#stream = stream;
|
|
399
|
+
const { writeBuffer, flushInterval } = this.#options;
|
|
400
|
+
this.#buffer = new BufferedStream({ writeBuffer, stream, flushInterval });
|
|
401
|
+
stream.on('error', (error) => {
|
|
402
|
+
const errorMsg = `Can't open log file: ${this.#file}, ${error.message}`;
|
|
403
|
+
this.emit('error', new Error(errorMsg));
|
|
257
404
|
});
|
|
258
|
-
|
|
259
|
-
this.emit('error', new Error(`Can't open log file: ${this.file}`));
|
|
260
|
-
});
|
|
261
|
-
await events.once(this, 'open');
|
|
405
|
+
await EventEmitter.once(stream, 'open');
|
|
262
406
|
return this;
|
|
263
407
|
}
|
|
264
408
|
|
|
265
409
|
async close() {
|
|
266
410
|
if (!this.active) return Promise.resolve();
|
|
267
|
-
if (!this
|
|
411
|
+
if (!this.#fsEnabled) {
|
|
268
412
|
this.active = false;
|
|
269
413
|
this.emit('close');
|
|
270
414
|
return Promise.resolve();
|
|
271
415
|
}
|
|
272
|
-
const
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
416
|
+
const stream = this.#stream;
|
|
417
|
+
if (stream.destroyed || stream.closed) return Promise.resolve();
|
|
418
|
+
clearTimeout(this.#rotationTimer);
|
|
419
|
+
this.#rotationTimer = null;
|
|
276
420
|
return new Promise((resolve, reject) => {
|
|
277
|
-
this.flush((
|
|
278
|
-
if (
|
|
421
|
+
this.flush((error) => {
|
|
422
|
+
if (error) return void reject(error);
|
|
279
423
|
this.active = false;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
424
|
+
this.#buffer
|
|
425
|
+
.close()
|
|
426
|
+
.then(() => {
|
|
427
|
+
const fileName = this.#file;
|
|
428
|
+
this.emit('close');
|
|
429
|
+
fs.stat(fileName, (error, stats) => {
|
|
430
|
+
if (error || stats.size > 0) {
|
|
431
|
+
return void resolve();
|
|
432
|
+
}
|
|
289
433
|
fsp.unlink(fileName).catch(() => {});
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
})
|
|
293
|
-
|
|
434
|
+
resolve();
|
|
435
|
+
});
|
|
436
|
+
})
|
|
437
|
+
.catch(reject);
|
|
294
438
|
});
|
|
295
439
|
});
|
|
296
440
|
}
|
|
297
441
|
|
|
298
442
|
async rotate() {
|
|
299
|
-
if (!this
|
|
443
|
+
if (!this.#keepDays) return;
|
|
300
444
|
const now = nowDays();
|
|
301
445
|
const finish = [];
|
|
302
446
|
try {
|
|
@@ -304,148 +448,86 @@ class Logger extends events.EventEmitter {
|
|
|
304
448
|
for (const fileName of files) {
|
|
305
449
|
if (metautil.fileExt(fileName) !== 'log') continue;
|
|
306
450
|
const fileAge = now - nameToDays(fileName);
|
|
307
|
-
if (fileAge < this
|
|
308
|
-
|
|
451
|
+
if (fileAge < this.#keepDays) continue;
|
|
452
|
+
const promise = fsp
|
|
453
|
+
.unlink(path.join(this.path, fileName))
|
|
454
|
+
.catch(() => {});
|
|
455
|
+
finish.push(promise);
|
|
309
456
|
}
|
|
310
457
|
await Promise.all(finish);
|
|
311
|
-
} catch (
|
|
312
|
-
process.stdout.write(`${
|
|
313
|
-
this.emit('error',
|
|
458
|
+
} catch (error) {
|
|
459
|
+
process.stdout.write(`${error.stack}\n`);
|
|
460
|
+
this.emit('error', error);
|
|
314
461
|
}
|
|
315
462
|
}
|
|
316
463
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
return `${time} ${id} ${mark} ${msg}`;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
formatFile(type, indent, ...args) {
|
|
336
|
-
const dateTime = new Date().toISOString();
|
|
337
|
-
const message = this.format(type, indent, ...args);
|
|
338
|
-
const msg = metautil.replace(message, '\n', LINE_SEPARATOR);
|
|
339
|
-
return `${dateTime} [${type}] ${msg}`;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
formatJson(type, indent, ...args) {
|
|
343
|
-
const log = {
|
|
344
|
-
timestamp: new Date().toISOString(),
|
|
345
|
-
workerId: this.workerId,
|
|
346
|
-
level: type,
|
|
347
|
-
message: null,
|
|
348
|
-
};
|
|
349
|
-
if (metautil.isError(args[0])) {
|
|
350
|
-
log.err = this.expandError(args[0]);
|
|
351
|
-
args = args.slice(1);
|
|
352
|
-
} else if (typeof args[0] === 'object') {
|
|
353
|
-
Object.assign(log, args[0]);
|
|
354
|
-
if (metautil.isError(log.err)) log.err = this.expandError(log.err);
|
|
355
|
-
if (metautil.isError(log.error)) log.error = this.expandError(log.error);
|
|
356
|
-
args = args.slice(1);
|
|
357
|
-
}
|
|
358
|
-
log.message = util.format(...args);
|
|
359
|
-
return JSON.stringify(log);
|
|
464
|
+
#createDir() {
|
|
465
|
+
return new Promise((resolve, reject) => {
|
|
466
|
+
fs.access(this.path, (error) => {
|
|
467
|
+
if (!error) resolve();
|
|
468
|
+
fs.mkdir(this.path, (error) => {
|
|
469
|
+
if (!error || error.code === 'EEXIST') {
|
|
470
|
+
return void resolve();
|
|
471
|
+
} else {
|
|
472
|
+
const error = new Error(`Can not create directory: ${this.path}`);
|
|
473
|
+
this.emit('error', error);
|
|
474
|
+
reject(error);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
});
|
|
360
479
|
}
|
|
361
480
|
|
|
362
|
-
write(
|
|
363
|
-
if (this
|
|
364
|
-
const line = this.json
|
|
365
|
-
? this.formatJson(
|
|
366
|
-
: this.formatPretty(
|
|
481
|
+
write(tag, indent, args) {
|
|
482
|
+
if (this.#toStdout[tag]) {
|
|
483
|
+
const line = this.#options.json
|
|
484
|
+
? this.#formatter.formatJson(tag, indent, args)
|
|
485
|
+
: this.#formatter.formatPretty(tag, indent, args);
|
|
367
486
|
process.stdout.write(line + '\n');
|
|
368
487
|
}
|
|
369
|
-
if (this
|
|
370
|
-
const line = this.json
|
|
371
|
-
? this.formatJson(
|
|
372
|
-
: this.formatFile(
|
|
488
|
+
if (this.#toFile[tag]) {
|
|
489
|
+
const line = this.#options.json
|
|
490
|
+
? this.#formatter.formatJson(tag, indent, args)
|
|
491
|
+
: this.#formatter.formatFile(tag, indent, args);
|
|
373
492
|
const buffer = Buffer.from(line + '\n');
|
|
374
|
-
this
|
|
375
|
-
this.bufferLength += buffer.length;
|
|
376
|
-
if (this.bufferLength >= this.writeBuffer) this.flush();
|
|
493
|
+
this.#buffer.write(buffer);
|
|
377
494
|
}
|
|
378
495
|
}
|
|
379
496
|
|
|
380
497
|
flush(callback) {
|
|
381
|
-
if (this.lock) {
|
|
382
|
-
if (callback) this.once('unlocked', callback);
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
if (this.buffer.length === 0) {
|
|
386
|
-
if (callback) callback();
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
498
|
if (!this.active) {
|
|
390
|
-
|
|
391
|
-
this.emit('error', err);
|
|
392
|
-
if (callback) callback(err);
|
|
499
|
+
if (callback) callback();
|
|
393
500
|
return;
|
|
394
501
|
}
|
|
395
|
-
if (!this
|
|
396
|
-
|
|
397
|
-
this.emit('error', err);
|
|
398
|
-
if (callback) callback(err);
|
|
502
|
+
if (!this.#buffer) {
|
|
503
|
+
if (callback) callback();
|
|
399
504
|
return;
|
|
400
505
|
}
|
|
401
|
-
this.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
this.bufferLength = 0;
|
|
405
|
-
this.stream.write(buffer, () => {
|
|
406
|
-
this.lock = false;
|
|
407
|
-
this.emit('unlocked');
|
|
408
|
-
if (callback) callback();
|
|
506
|
+
this.#buffer.flush((error) => {
|
|
507
|
+
if (error) this.emit('error', error);
|
|
508
|
+
if (callback) callback(error);
|
|
409
509
|
});
|
|
410
510
|
}
|
|
411
511
|
|
|
412
|
-
normalizeStack(stack) {
|
|
413
|
-
if (!stack) return 'no data to log';
|
|
414
|
-
let res = metautil.replace(stack, STACK_AT, '');
|
|
415
|
-
if (this.home) res = metautil.replace(res, this.home, '');
|
|
416
|
-
return res;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
expandError(err) {
|
|
420
|
-
return {
|
|
421
|
-
message: err.message,
|
|
422
|
-
stack: this.normalizeStack(err.stack),
|
|
423
|
-
...err,
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
|
|
427
512
|
#setupCrashHandling() {
|
|
428
513
|
const exitHandler = () => {
|
|
429
|
-
this.flush();
|
|
514
|
+
if (this.active) this.flush();
|
|
430
515
|
};
|
|
431
516
|
process.on('SIGTERM', exitHandler);
|
|
432
517
|
process.on('SIGINT', exitHandler);
|
|
433
518
|
process.on('SIGUSR1', exitHandler);
|
|
434
519
|
process.on('SIGUSR2', exitHandler);
|
|
435
|
-
process.on('uncaughtException',
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
});
|
|
439
|
-
process.on('unhandledRejection', (reason) => {
|
|
440
|
-
this.write('error', 0, 'Unhandled Rejection:', reason);
|
|
441
|
-
this.flush();
|
|
442
|
-
});
|
|
443
|
-
process.on('exit', () => {
|
|
444
|
-
this.flush();
|
|
445
|
-
});
|
|
520
|
+
process.on('uncaughtException', exitHandler);
|
|
521
|
+
process.on('unhandledRejection', exitHandler);
|
|
522
|
+
process.on('exit', exitHandler);
|
|
446
523
|
}
|
|
447
524
|
}
|
|
448
525
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
526
|
+
module.exports = {
|
|
527
|
+
Logger,
|
|
528
|
+
Console,
|
|
529
|
+
BufferedStream,
|
|
530
|
+
Formatter,
|
|
531
|
+
nowDays,
|
|
532
|
+
nameToDays,
|
|
533
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metalog",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0-prerelease",
|
|
4
4
|
"author": "Timur Shemsedinov <timur.shemsedinov@gmail.com>",
|
|
5
5
|
"description": "Logger for Metarhia",
|
|
6
6
|
"license": "MIT",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"metautil": "^5.3.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@types/node": "^24.3.
|
|
60
|
-
"eslint": "^9.
|
|
59
|
+
"@types/node": "^24.3.1",
|
|
60
|
+
"eslint": "^9.35.0",
|
|
61
61
|
"eslint-config-metarhia": "^9.1.3",
|
|
62
62
|
"prettier": "^3.6.2",
|
|
63
63
|
"typescript": "^5.9.2"
|