metalog 3.1.18 → 4.0.1-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 +299 -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,132 +323,125 @@ 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;
|
|
193
|
-
this.path = options.path;
|
|
358
|
+
this.#options = options;
|
|
359
|
+
const { workerId, createStream, keepDays, home, crash, json } = options;
|
|
360
|
+
const { toFile = LOG_TAGS, toStdout = LOG_TAGS } = options;
|
|
194
361
|
this.path = path.resolve(options.path);
|
|
195
|
-
this.workerId = `W${workerId}`;
|
|
196
|
-
this.createStream = createStream;
|
|
197
|
-
this.writeInterval = writeInterval || DEFAULT_WRITE_INTERVAL;
|
|
198
|
-
this.writeBuffer = writeBuffer || DEFAULT_BUFFER_SIZE;
|
|
199
|
-
this.keepDays = keepDays || DEFAULT_KEEP_DAYS;
|
|
200
362
|
this.home = home ? path.resolve(home) : undefined;
|
|
201
|
-
this.
|
|
202
|
-
this
|
|
203
|
-
this
|
|
204
|
-
this
|
|
205
|
-
this
|
|
206
|
-
this
|
|
207
|
-
this.bufferLength = 0;
|
|
208
|
-
this.file = '';
|
|
209
|
-
this.toFile = logTypes(toFile);
|
|
210
|
-
this.fsEnabled = toFile.length !== 0;
|
|
211
|
-
this.toStdout = logTypes(toStdout);
|
|
212
|
-
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;
|
|
213
369
|
if (crash === 'flush') this.#setupCrashHandling();
|
|
370
|
+
this.#fsEnabled = toFile.length !== 0;
|
|
371
|
+
this.#buffer = null;
|
|
372
|
+
const formatterOptions = { json, worker: this.#worker, home: this.home };
|
|
373
|
+
this.#formatter = new Formatter(formatterOptions);
|
|
214
374
|
return this.open();
|
|
215
375
|
}
|
|
216
376
|
|
|
217
|
-
|
|
218
|
-
return new
|
|
219
|
-
fs.access(this.path, (err) => {
|
|
220
|
-
if (!err) resolve();
|
|
221
|
-
fs.mkdir(this.path, (err) => {
|
|
222
|
-
if (!err || err.code === 'EEXIST') return void resolve();
|
|
223
|
-
const error = new Error(`Can not create directory: ${this.path}\n`);
|
|
224
|
-
this.emit('error', error);
|
|
225
|
-
reject();
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
});
|
|
377
|
+
static async create(options) {
|
|
378
|
+
return new Logger(options);
|
|
229
379
|
}
|
|
230
380
|
|
|
231
381
|
async open() {
|
|
232
382
|
if (this.active) return this;
|
|
233
383
|
this.active = true;
|
|
234
|
-
if (!this
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
await this.createLogDir();
|
|
239
|
-
const fileName = metautil.nowDate() + '-' + this.workerId + '.log';
|
|
240
|
-
this.file = path.join(this.path, fileName);
|
|
384
|
+
if (!this.#fsEnabled) return this;
|
|
385
|
+
await this.#createDir();
|
|
386
|
+
const fileName = metautil.nowDate() + '-' + this.#worker + '.log';
|
|
387
|
+
this.#file = path.join(this.path, fileName);
|
|
241
388
|
const nextReopen = getNextReopen();
|
|
242
|
-
this
|
|
389
|
+
this.#rotationTimer = setTimeout(() => {
|
|
243
390
|
this.once('close', () => {
|
|
244
391
|
this.open();
|
|
245
392
|
});
|
|
246
|
-
this.close().catch((
|
|
247
|
-
|
|
248
|
-
this.emit('error', err);
|
|
393
|
+
this.close().catch((error) => {
|
|
394
|
+
this.emit('error', error);
|
|
249
395
|
});
|
|
250
396
|
}, nextReopen);
|
|
251
|
-
if (this
|
|
252
|
-
|
|
253
|
-
this
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
this.
|
|
397
|
+
if (this.#keepDays) await this.rotate();
|
|
398
|
+
const stream = this.#createStream(this.#file, { flags: 'a' });
|
|
399
|
+
this.#stream = stream;
|
|
400
|
+
const { writeBuffer, flushInterval } = this.#options;
|
|
401
|
+
this.#buffer = new BufferedStream({ writeBuffer, stream, flushInterval });
|
|
402
|
+
stream.on('error', (error) => {
|
|
403
|
+
const errorMsg = `Can't open log file: ${this.#file}, ${error.message}`;
|
|
404
|
+
this.emit('error', new Error(errorMsg));
|
|
258
405
|
});
|
|
259
|
-
|
|
260
|
-
this.emit('error', new Error(`Can't open log file: ${this.file}`));
|
|
261
|
-
});
|
|
262
|
-
await events.once(this, 'open');
|
|
406
|
+
await EventEmitter.once(stream, 'open');
|
|
263
407
|
return this;
|
|
264
408
|
}
|
|
265
409
|
|
|
266
410
|
async close() {
|
|
267
411
|
if (!this.active) return Promise.resolve();
|
|
268
|
-
if (!this
|
|
412
|
+
if (!this.#fsEnabled) {
|
|
269
413
|
this.active = false;
|
|
270
414
|
this.emit('close');
|
|
271
415
|
return Promise.resolve();
|
|
272
416
|
}
|
|
273
|
-
const
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
417
|
+
const stream = this.#stream;
|
|
418
|
+
if (stream.destroyed || stream.closed) return Promise.resolve();
|
|
419
|
+
clearTimeout(this.#rotationTimer);
|
|
420
|
+
this.#rotationTimer = null;
|
|
277
421
|
return new Promise((resolve, reject) => {
|
|
278
|
-
this.flush((
|
|
279
|
-
if (
|
|
422
|
+
this.flush((error) => {
|
|
423
|
+
if (error) return void reject(error);
|
|
280
424
|
this.active = false;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
425
|
+
this.#buffer
|
|
426
|
+
.close()
|
|
427
|
+
.then(() => {
|
|
428
|
+
const fileName = this.#file;
|
|
429
|
+
this.emit('close');
|
|
430
|
+
fs.stat(fileName, (error, stats) => {
|
|
431
|
+
if (error || stats.size > 0) {
|
|
432
|
+
return void resolve();
|
|
433
|
+
}
|
|
290
434
|
fsp.unlink(fileName).catch(() => {});
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
})
|
|
294
|
-
|
|
435
|
+
resolve();
|
|
436
|
+
});
|
|
437
|
+
})
|
|
438
|
+
.catch(reject);
|
|
295
439
|
});
|
|
296
440
|
});
|
|
297
441
|
}
|
|
298
442
|
|
|
299
443
|
async rotate() {
|
|
300
|
-
if (!this
|
|
444
|
+
if (!this.#keepDays) return;
|
|
301
445
|
const now = nowDays();
|
|
302
446
|
const finish = [];
|
|
303
447
|
try {
|
|
@@ -305,150 +449,85 @@ class Logger extends events.EventEmitter {
|
|
|
305
449
|
for (const fileName of files) {
|
|
306
450
|
if (metautil.fileExt(fileName) !== 'log') continue;
|
|
307
451
|
const fileAge = now - nameToDays(fileName);
|
|
308
|
-
if (fileAge < this
|
|
452
|
+
if (fileAge < this.#keepDays) continue;
|
|
309
453
|
const filePath = path.join(this.path, fileName);
|
|
310
454
|
const promise = fsp.unlink(filePath).catch(() => {});
|
|
311
455
|
finish.push(promise);
|
|
312
456
|
}
|
|
313
457
|
await Promise.allSettled(finish);
|
|
314
|
-
} catch (
|
|
315
|
-
process.stdout.write(`${
|
|
316
|
-
this.emit('error',
|
|
458
|
+
} catch (error) {
|
|
459
|
+
process.stdout.write(`${error.stack}\n`);
|
|
460
|
+
this.emit('error', error);
|
|
317
461
|
}
|
|
318
462
|
}
|
|
319
463
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return `${time} ${id} ${mark} ${msg}`;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
formatFile(type, indent, ...args) {
|
|
339
|
-
const dateTime = new Date().toISOString();
|
|
340
|
-
const message = this.format(type, indent, ...args);
|
|
341
|
-
const msg = metautil.replace(message, '\n', LINE_SEPARATOR);
|
|
342
|
-
return `${dateTime} [${type}] ${msg}`;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
formatJson(type, indent, ...args) {
|
|
346
|
-
const log = {
|
|
347
|
-
timestamp: new Date().toISOString(),
|
|
348
|
-
workerId: this.workerId,
|
|
349
|
-
level: type,
|
|
350
|
-
message: null,
|
|
351
|
-
};
|
|
352
|
-
if (metautil.isError(args[0])) {
|
|
353
|
-
log.err = this.expandError(args[0]);
|
|
354
|
-
args = args.slice(1);
|
|
355
|
-
} else if (typeof args[0] === 'object') {
|
|
356
|
-
Object.assign(log, args[0]);
|
|
357
|
-
if (metautil.isError(log.err)) log.err = this.expandError(log.err);
|
|
358
|
-
if (metautil.isError(log.error)) log.error = this.expandError(log.error);
|
|
359
|
-
args = args.slice(1);
|
|
360
|
-
}
|
|
361
|
-
log.message = util.format(...args);
|
|
362
|
-
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
|
+
});
|
|
363
479
|
}
|
|
364
480
|
|
|
365
|
-
write(
|
|
366
|
-
if (this
|
|
367
|
-
const line = this.json
|
|
368
|
-
? this.formatJson(
|
|
369
|
-
: 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);
|
|
370
486
|
process.stdout.write(line + '\n');
|
|
371
487
|
}
|
|
372
|
-
if (this
|
|
373
|
-
const line = this.json
|
|
374
|
-
? this.formatJson(
|
|
375
|
-
: 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);
|
|
376
492
|
const buffer = Buffer.from(line + '\n');
|
|
377
|
-
this
|
|
378
|
-
this.bufferLength += buffer.length;
|
|
379
|
-
if (this.bufferLength >= this.writeBuffer) this.flush();
|
|
493
|
+
this.#buffer.write(buffer);
|
|
380
494
|
}
|
|
381
495
|
}
|
|
382
496
|
|
|
383
497
|
flush(callback) {
|
|
384
|
-
if (this.lock) {
|
|
385
|
-
if (callback) this.once('unlocked', callback);
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
if (this.buffer.length === 0) {
|
|
389
|
-
if (callback) callback();
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
498
|
if (!this.active) {
|
|
393
|
-
|
|
394
|
-
this.emit('error', err);
|
|
395
|
-
if (callback) callback(err);
|
|
499
|
+
if (callback) callback();
|
|
396
500
|
return;
|
|
397
501
|
}
|
|
398
|
-
if (!this
|
|
399
|
-
|
|
400
|
-
this.emit('error', err);
|
|
401
|
-
if (callback) callback(err);
|
|
502
|
+
if (!this.#buffer) {
|
|
503
|
+
if (callback) callback();
|
|
402
504
|
return;
|
|
403
505
|
}
|
|
404
|
-
this.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
this.bufferLength = 0;
|
|
408
|
-
this.stream.write(buffer, () => {
|
|
409
|
-
this.lock = false;
|
|
410
|
-
this.emit('unlocked');
|
|
411
|
-
if (callback) callback();
|
|
506
|
+
this.#buffer.flush((error) => {
|
|
507
|
+
if (error) this.emit('error', error);
|
|
508
|
+
if (callback) callback(error);
|
|
412
509
|
});
|
|
413
510
|
}
|
|
414
511
|
|
|
415
|
-
normalizeStack(stack) {
|
|
416
|
-
if (!stack) return 'no data to log';
|
|
417
|
-
let res = metautil.replace(stack, STACK_AT, '');
|
|
418
|
-
if (this.home) res = metautil.replace(res, this.home, '');
|
|
419
|
-
return res;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
expandError(err) {
|
|
423
|
-
return {
|
|
424
|
-
message: err.message,
|
|
425
|
-
stack: this.normalizeStack(err.stack),
|
|
426
|
-
...err,
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
512
|
#setupCrashHandling() {
|
|
431
513
|
const exitHandler = () => {
|
|
432
|
-
this.flush();
|
|
514
|
+
if (this.active) this.flush();
|
|
433
515
|
};
|
|
434
516
|
process.on('SIGTERM', exitHandler);
|
|
435
517
|
process.on('SIGINT', exitHandler);
|
|
436
518
|
process.on('SIGUSR1', exitHandler);
|
|
437
519
|
process.on('SIGUSR2', exitHandler);
|
|
438
|
-
process.on('uncaughtException',
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
});
|
|
442
|
-
process.on('unhandledRejection', (reason) => {
|
|
443
|
-
this.write('error', 0, 'Unhandled Rejection:', reason);
|
|
444
|
-
this.flush();
|
|
445
|
-
});
|
|
446
|
-
process.on('exit', () => {
|
|
447
|
-
this.flush();
|
|
448
|
-
});
|
|
520
|
+
process.on('uncaughtException', exitHandler);
|
|
521
|
+
process.on('unhandledRejection', exitHandler);
|
|
522
|
+
process.on('exit', exitHandler);
|
|
449
523
|
}
|
|
450
524
|
}
|
|
451
525
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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.1-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"
|