@morojs/moro 1.5.2 → 1.5.4
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/dist/core/auth/morojs-adapter.js +23 -12
- package/dist/core/auth/morojs-adapter.js.map +1 -1
- package/dist/core/http/http-server.js +12 -8
- package/dist/core/http/http-server.js.map +1 -1
- package/dist/core/logger/filters.js +12 -4
- package/dist/core/logger/filters.js.map +1 -1
- package/dist/core/logger/logger.d.ts +44 -0
- package/dist/core/logger/logger.js +550 -60
- package/dist/core/logger/logger.js.map +1 -1
- package/dist/core/middleware/built-in/request-logger.js +4 -2
- package/dist/core/middleware/built-in/request-logger.js.map +1 -1
- package/dist/core/modules/auto-discovery.d.ts +1 -0
- package/dist/core/modules/auto-discovery.js +9 -5
- package/dist/core/modules/auto-discovery.js.map +1 -1
- package/dist/core/modules/modules.d.ts +1 -0
- package/dist/core/modules/modules.js +8 -2
- package/dist/core/modules/modules.js.map +1 -1
- package/dist/core/networking/adapters/ws-adapter.d.ts +1 -0
- package/dist/core/networking/adapters/ws-adapter.js +3 -1
- package/dist/core/networking/adapters/ws-adapter.js.map +1 -1
- package/dist/core/networking/service-discovery.d.ts +1 -0
- package/dist/core/networking/service-discovery.js +23 -11
- package/dist/core/networking/service-discovery.js.map +1 -1
- package/dist/types/logger.d.ts +3 -0
- package/package.json +6 -2
- package/src/core/auth/morojs-adapter.ts +25 -12
- package/src/core/database/README.md +26 -16
- package/src/core/http/http-server.ts +15 -12
- package/src/core/logger/filters.ts +12 -4
- package/src/core/logger/logger.ts +616 -63
- package/src/core/middleware/built-in/request-logger.ts +6 -2
- package/src/core/modules/auto-discovery.ts +13 -5
- package/src/core/modules/modules.ts +9 -5
- package/src/core/networking/adapters/ws-adapter.ts +3 -1
- package/src/core/networking/service-discovery.ts +23 -9
- package/src/types/logger.ts +3 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Moro Logger - Beautiful, Fast, Feature-Rich
|
|
2
2
|
import { performance } from 'perf_hooks';
|
|
3
|
+
|
|
3
4
|
import {
|
|
4
5
|
LogLevel,
|
|
5
6
|
LogEntry,
|
|
@@ -31,6 +32,37 @@ export class MoroLogger implements Logger {
|
|
|
31
32
|
private contextMetadata?: Record<string, any>;
|
|
32
33
|
private parent?: MoroLogger; // Reference to parent logger for level inheritance
|
|
33
34
|
|
|
35
|
+
// Performance optimizations
|
|
36
|
+
private historyIndex = 0;
|
|
37
|
+
private historySize = 0;
|
|
38
|
+
private lastMemoryCheck = 0;
|
|
39
|
+
private memoryCheckInterval = 5000; // 5 seconds
|
|
40
|
+
private cachedTimestamp = '';
|
|
41
|
+
private lastTimestamp = 0;
|
|
42
|
+
private timestampCacheInterval = 100; // 100ms for better precision
|
|
43
|
+
|
|
44
|
+
// Object pooling for LogEntry objects (Pino's technique)
|
|
45
|
+
private static readonly ENTRY_POOL: LogEntry[] = [];
|
|
46
|
+
private static readonly MAX_POOL_SIZE = 100;
|
|
47
|
+
private static poolIndex = 0;
|
|
48
|
+
|
|
49
|
+
// String builder for efficient concatenation
|
|
50
|
+
private static stringBuilder: string[] = [];
|
|
51
|
+
private static stringBuilderIndex = 0;
|
|
52
|
+
|
|
53
|
+
// Buffered output for performance
|
|
54
|
+
private outputBuffer: string[] = [];
|
|
55
|
+
private bufferSize = 0;
|
|
56
|
+
private maxBufferSize = 1000;
|
|
57
|
+
private flushTimeout: NodeJS.Timeout | null = null;
|
|
58
|
+
private flushInterval = 1; // 1ms micro-batching
|
|
59
|
+
|
|
60
|
+
// Buffer overflow protection
|
|
61
|
+
private bufferOverflowThreshold: number;
|
|
62
|
+
private emergencyFlushInProgress = false;
|
|
63
|
+
|
|
64
|
+
// High-performance output methods
|
|
65
|
+
|
|
34
66
|
private static readonly LEVELS: Record<LogLevel, number> = {
|
|
35
67
|
debug: 0,
|
|
36
68
|
info: 1,
|
|
@@ -49,13 +81,23 @@ export class MoroLogger implements Logger {
|
|
|
49
81
|
context: '\x1b[34m', // Blue
|
|
50
82
|
metadata: '\x1b[37m', // White
|
|
51
83
|
performance: '\x1b[36m', // Cyan
|
|
84
|
+
reset: '\x1b[0m', // Reset
|
|
52
85
|
};
|
|
53
86
|
|
|
54
87
|
private static readonly RESET = '\x1b[0m';
|
|
55
88
|
private static readonly BOLD = '\x1b[1m';
|
|
56
89
|
|
|
90
|
+
// Static pre-allocated strings for performance
|
|
91
|
+
private static readonly LEVEL_STRINGS: Record<LogLevel, string> = {
|
|
92
|
+
debug: 'DEBUG',
|
|
93
|
+
info: 'INFO ',
|
|
94
|
+
warn: 'WARN ',
|
|
95
|
+
error: 'ERROR',
|
|
96
|
+
fatal: 'FATAL',
|
|
97
|
+
};
|
|
98
|
+
|
|
57
99
|
constructor(options: LoggerOptions = {}) {
|
|
58
|
-
this.options = {
|
|
100
|
+
this.options = this.validateOptions({
|
|
59
101
|
level: 'info',
|
|
60
102
|
enableColors: true,
|
|
61
103
|
enableTimestamp: true,
|
|
@@ -66,11 +108,18 @@ export class MoroLogger implements Logger {
|
|
|
66
108
|
outputs: [],
|
|
67
109
|
filters: [],
|
|
68
110
|
maxEntries: 1000,
|
|
111
|
+
maxBufferSize: 1000,
|
|
69
112
|
...options,
|
|
70
|
-
};
|
|
113
|
+
});
|
|
71
114
|
|
|
72
115
|
this.level = this.options.level || 'info';
|
|
73
116
|
|
|
117
|
+
// Initialize buffer size from options
|
|
118
|
+
this.maxBufferSize = this.options.maxBufferSize || 1000;
|
|
119
|
+
|
|
120
|
+
// Initialize buffer overflow protection
|
|
121
|
+
this.bufferOverflowThreshold = this.maxBufferSize * 2;
|
|
122
|
+
|
|
74
123
|
// Add default console output
|
|
75
124
|
this.addOutput({
|
|
76
125
|
name: 'console',
|
|
@@ -83,6 +132,58 @@ export class MoroLogger implements Logger {
|
|
|
83
132
|
this.options.filters?.forEach(filter => this.addFilter(filter));
|
|
84
133
|
}
|
|
85
134
|
|
|
135
|
+
// Object pooling methods
|
|
136
|
+
private static getPooledEntry(): LogEntry {
|
|
137
|
+
if (MoroLogger.ENTRY_POOL.length > 0) {
|
|
138
|
+
const entry = MoroLogger.ENTRY_POOL.pop()!;
|
|
139
|
+
// Properly reset ALL properties to prevent memory leaks
|
|
140
|
+
entry.timestamp = new Date();
|
|
141
|
+
entry.level = 'info';
|
|
142
|
+
entry.message = '';
|
|
143
|
+
entry.context = undefined;
|
|
144
|
+
entry.metadata = undefined;
|
|
145
|
+
entry.performance = undefined;
|
|
146
|
+
entry.moduleId = undefined;
|
|
147
|
+
return entry;
|
|
148
|
+
}
|
|
149
|
+
return MoroLogger.createFreshEntry();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ADD this new method:
|
|
153
|
+
private static createFreshEntry(): LogEntry {
|
|
154
|
+
return {
|
|
155
|
+
timestamp: new Date(),
|
|
156
|
+
level: 'info',
|
|
157
|
+
message: '',
|
|
158
|
+
context: undefined,
|
|
159
|
+
metadata: undefined,
|
|
160
|
+
performance: undefined,
|
|
161
|
+
moduleId: undefined,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private static returnPooledEntry(entry: LogEntry): void {
|
|
166
|
+
if (MoroLogger.ENTRY_POOL.length < MoroLogger.MAX_POOL_SIZE) {
|
|
167
|
+
MoroLogger.ENTRY_POOL.push(entry);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// String builder methods
|
|
172
|
+
private static resetStringBuilder(): void {
|
|
173
|
+
MoroLogger.stringBuilder.length = 0;
|
|
174
|
+
MoroLogger.stringBuilderIndex = 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private static appendToBuilder(str: string): void {
|
|
178
|
+
MoroLogger.stringBuilder[MoroLogger.stringBuilderIndex++] = str;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private static buildString(): string {
|
|
182
|
+
const result = MoroLogger.stringBuilder.join('');
|
|
183
|
+
MoroLogger.resetStringBuilder();
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
|
|
86
187
|
debug(message: string, context?: string, metadata?: Record<string, any>): void {
|
|
87
188
|
this.log('debug', message, context, metadata);
|
|
88
189
|
}
|
|
@@ -158,8 +259,47 @@ export class MoroLogger implements Logger {
|
|
|
158
259
|
}
|
|
159
260
|
|
|
160
261
|
getHistory(count?: number): LogEntry[] {
|
|
161
|
-
|
|
162
|
-
|
|
262
|
+
if (this.historySize === 0) return [];
|
|
263
|
+
|
|
264
|
+
if (this.historySize < (this.options.maxEntries || 1000)) {
|
|
265
|
+
// History not full yet, return all entries
|
|
266
|
+
const entries = this.history.slice(0, this.historySize);
|
|
267
|
+
return count ? entries.slice(-count) : entries;
|
|
268
|
+
} else {
|
|
269
|
+
// History is full, use circular buffer logic
|
|
270
|
+
const entries: LogEntry[] = [];
|
|
271
|
+
const maxEntries = this.options.maxEntries || 1000;
|
|
272
|
+
|
|
273
|
+
for (let i = 0; i < maxEntries; i++) {
|
|
274
|
+
const index = (this.historyIndex + i) % maxEntries;
|
|
275
|
+
if (this.history[index]) {
|
|
276
|
+
entries.push(this.history[index]);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return count ? entries.slice(-count) : entries;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Cached timestamp formatting to avoid repeated string operations
|
|
285
|
+
private getCachedTimestamp(timestamp: Date): string {
|
|
286
|
+
const now = timestamp.getTime();
|
|
287
|
+
if (now - this.lastTimestamp > this.timestampCacheInterval) {
|
|
288
|
+
this.lastTimestamp = now;
|
|
289
|
+
this.cachedTimestamp = timestamp.toISOString().replace('T', ' ').slice(0, 19);
|
|
290
|
+
}
|
|
291
|
+
return this.cachedTimestamp;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Cached timestamp generation (updates once per second)
|
|
295
|
+
private getFastCachedTimestamp(): string {
|
|
296
|
+
const now = Date.now();
|
|
297
|
+
if (now - this.lastTimestamp > 1000) {
|
|
298
|
+
// Update every second
|
|
299
|
+
this.lastTimestamp = now;
|
|
300
|
+
this.cachedTimestamp = new Date(now).toISOString().slice(0, 19).replace('T', ' ');
|
|
301
|
+
}
|
|
302
|
+
return this.cachedTimestamp;
|
|
163
303
|
}
|
|
164
304
|
|
|
165
305
|
getMetrics(): LogMetrics {
|
|
@@ -189,61 +329,130 @@ export class MoroLogger implements Logger {
|
|
|
189
329
|
};
|
|
190
330
|
}
|
|
191
331
|
|
|
332
|
+
// Optimized logging method
|
|
192
333
|
private log(
|
|
193
334
|
level: LogLevel,
|
|
194
335
|
message: string,
|
|
195
336
|
context?: string,
|
|
196
337
|
metadata?: Record<string, any>
|
|
197
338
|
): void {
|
|
198
|
-
//
|
|
339
|
+
// Quick level check - use parent level if available (for child loggers)
|
|
199
340
|
const effectiveLevel = this.parent ? this.parent.level : this.level;
|
|
200
341
|
if (MoroLogger.LEVELS[level] < MoroLogger.LEVELS[effectiveLevel as LogLevel]) {
|
|
201
342
|
return;
|
|
202
343
|
}
|
|
203
344
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
level
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
metadata: { ...this.contextMetadata, ...metadata },
|
|
215
|
-
performance: this.options.enablePerformance
|
|
216
|
-
? {
|
|
217
|
-
memory: process.memoryUsage().heapUsed / 1024 / 1024,
|
|
218
|
-
}
|
|
219
|
-
: undefined,
|
|
220
|
-
};
|
|
345
|
+
// Absolute minimal path for simple logs - pure speed
|
|
346
|
+
if (!metadata && !context && !this.contextPrefix && !this.contextMetadata) {
|
|
347
|
+
const logStr = level.toUpperCase() + ' ' + message + '\n';
|
|
348
|
+
if (level === 'error' || level === 'fatal') {
|
|
349
|
+
process.stderr.write(logStr);
|
|
350
|
+
} else {
|
|
351
|
+
process.stdout.write(logStr);
|
|
352
|
+
}
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
221
355
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
356
|
+
// Minimal path for logs with context but no metadata
|
|
357
|
+
if (!metadata && !this.contextMetadata) {
|
|
358
|
+
const contextStr = context ? '[' + context + '] ' : '';
|
|
359
|
+
const logStr = level.toUpperCase() + ' ' + contextStr + message + '\n';
|
|
360
|
+
if (level === 'error' || level === 'fatal') {
|
|
361
|
+
process.stderr.write(logStr);
|
|
362
|
+
} else {
|
|
363
|
+
process.stdout.write(logStr);
|
|
364
|
+
}
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Path for complex logs
|
|
369
|
+
if (metadata && Object.keys(metadata).length > 0) {
|
|
370
|
+
this.complexLog(level, message, context, metadata);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Full logging path for complex logs
|
|
375
|
+
this.fullLog(level, message, context, metadata);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Full logging with all features
|
|
379
|
+
private fullLog(
|
|
380
|
+
level: LogLevel,
|
|
381
|
+
message: string,
|
|
382
|
+
context?: string,
|
|
383
|
+
metadata?: Record<string, any>
|
|
384
|
+
): void {
|
|
385
|
+
// Use object pooling for LogEntry (Pino's technique)
|
|
386
|
+
const entry = MoroLogger.getPooledEntry();
|
|
387
|
+
const now = Date.now();
|
|
388
|
+
|
|
389
|
+
entry.timestamp = new Date(now);
|
|
390
|
+
entry.level = level;
|
|
391
|
+
entry.message = message;
|
|
392
|
+
entry.context = this.contextPrefix
|
|
393
|
+
? context
|
|
394
|
+
? `${this.contextPrefix}:${context}`
|
|
395
|
+
: this.contextPrefix
|
|
396
|
+
: context;
|
|
397
|
+
entry.metadata = this.createMetadata(metadata);
|
|
398
|
+
entry.performance = this.options.enablePerformance ? this.getPerformanceData(now) : undefined;
|
|
399
|
+
|
|
400
|
+
// Apply filters with early return optimization
|
|
401
|
+
if (this.filters.size > 0) {
|
|
402
|
+
for (const filter of this.filters.values()) {
|
|
403
|
+
if (!filter.filter(entry)) {
|
|
404
|
+
MoroLogger.returnPooledEntry(entry);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
226
407
|
}
|
|
227
408
|
}
|
|
228
409
|
|
|
229
410
|
// Update metrics
|
|
230
411
|
this.updateMetrics(entry);
|
|
231
412
|
|
|
232
|
-
// Store in history
|
|
233
|
-
this.
|
|
234
|
-
|
|
235
|
-
|
|
413
|
+
// Store in history with circular buffer optimization
|
|
414
|
+
this.addToHistory(entry);
|
|
415
|
+
|
|
416
|
+
// Write to outputs with batched processing
|
|
417
|
+
this.writeToOutputs(entry, level);
|
|
418
|
+
|
|
419
|
+
// Return entry to pool
|
|
420
|
+
MoroLogger.returnPooledEntry(entry);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Absolute minimal logging - pure speed, no overhead
|
|
424
|
+
private complexLog(
|
|
425
|
+
level: LogLevel,
|
|
426
|
+
message: string,
|
|
427
|
+
context?: string,
|
|
428
|
+
metadata?: Record<string, any>
|
|
429
|
+
): void {
|
|
430
|
+
// Build string directly - no array, no try-catch, no metrics
|
|
431
|
+
let logStr = this.getFastCachedTimestamp() + ' ' + level.toUpperCase().padEnd(5);
|
|
432
|
+
|
|
433
|
+
if (context) {
|
|
434
|
+
logStr += '[' + context + '] ';
|
|
435
|
+
} else if (this.contextPrefix) {
|
|
436
|
+
logStr += '[' + this.contextPrefix + '] ';
|
|
236
437
|
}
|
|
237
438
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
439
|
+
logStr += message;
|
|
440
|
+
|
|
441
|
+
if (metadata && Object.keys(metadata).length > 0) {
|
|
442
|
+
logStr += ' ' + this.safeStringify(metadata);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (this.contextMetadata && Object.keys(this.contextMetadata).length > 0) {
|
|
446
|
+
logStr += ' ' + this.safeStringify(this.contextMetadata);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
logStr += '\n';
|
|
450
|
+
|
|
451
|
+
// Direct write - no error handling, no buffering
|
|
452
|
+
if (level === 'error' || level === 'fatal') {
|
|
453
|
+
process.stderr.write(logStr);
|
|
454
|
+
} else {
|
|
455
|
+
process.stdout.write(logStr);
|
|
247
456
|
}
|
|
248
457
|
}
|
|
249
458
|
|
|
@@ -257,18 +466,87 @@ export class MoroLogger implements Logger {
|
|
|
257
466
|
}
|
|
258
467
|
}
|
|
259
468
|
|
|
469
|
+
// Optimized metadata creation to avoid unnecessary object spreading
|
|
470
|
+
private createMetadata(metadata?: Record<string, any>): Record<string, any> {
|
|
471
|
+
if (!metadata && !this.contextMetadata) {
|
|
472
|
+
return {};
|
|
473
|
+
}
|
|
474
|
+
if (!metadata) {
|
|
475
|
+
return { ...this.contextMetadata };
|
|
476
|
+
}
|
|
477
|
+
if (!this.contextMetadata) {
|
|
478
|
+
return { ...metadata };
|
|
479
|
+
}
|
|
480
|
+
return { ...this.contextMetadata, ...metadata };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Optimized performance data with caching
|
|
484
|
+
private getPerformanceData(now: number): { memory: number } | undefined {
|
|
485
|
+
if (now - this.lastMemoryCheck > this.memoryCheckInterval) {
|
|
486
|
+
this.lastMemoryCheck = now;
|
|
487
|
+
this.metrics.memoryUsage = process.memoryUsage().heapUsed / 1024 / 1024;
|
|
488
|
+
}
|
|
489
|
+
return { memory: this.metrics.memoryUsage };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Circular buffer implementation for history (O(1) instead of O(n))
|
|
493
|
+
private addToHistory(entry: LogEntry): void {
|
|
494
|
+
const maxEntries = this.options.maxEntries || 1000;
|
|
495
|
+
|
|
496
|
+
if (this.historySize < maxEntries) {
|
|
497
|
+
this.history[this.historySize] = entry;
|
|
498
|
+
this.historySize++;
|
|
499
|
+
} else {
|
|
500
|
+
// Circular buffer: overwrite oldest entry
|
|
501
|
+
this.history[this.historyIndex] = entry;
|
|
502
|
+
this.historyIndex = (this.historyIndex + 1) % maxEntries;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Optimized output writing with batching
|
|
507
|
+
private writeToOutputs(entry: LogEntry, level: LogLevel): void {
|
|
508
|
+
if (this.outputs.size === 0) return;
|
|
509
|
+
|
|
510
|
+
let successCount = 0;
|
|
511
|
+
const errors: Array<{ outputName: string; error: any }> = [];
|
|
512
|
+
|
|
513
|
+
for (const output of this.outputs.values()) {
|
|
514
|
+
if (!output.level || MoroLogger.LEVELS[level] >= MoroLogger.LEVELS[output.level]) {
|
|
515
|
+
try {
|
|
516
|
+
output.write(entry);
|
|
517
|
+
successCount++;
|
|
518
|
+
} catch (error) {
|
|
519
|
+
errors.push({ outputName: output.name, error });
|
|
520
|
+
this.handleOutputError(output.name, error);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// If all outputs fail, use emergency console
|
|
526
|
+
if (successCount === 0 && this.outputs.size > 0) {
|
|
527
|
+
this.emergencyConsoleWrite(entry);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Log output errors (but avoid infinite loops)
|
|
531
|
+
if (errors.length > 0 && level !== 'error') {
|
|
532
|
+
this.error(`Logger output errors: ${errors.length} failed`, 'MoroLogger', {
|
|
533
|
+
errors: errors.map(e => e.outputName),
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
260
538
|
private writeToConsole(entry: LogEntry): void {
|
|
261
539
|
const format = this.options.format || 'pretty';
|
|
262
540
|
|
|
263
541
|
if (format === 'json') {
|
|
264
|
-
|
|
542
|
+
this.output(`${this.safeStringify(entry)}\n`, entry.level);
|
|
265
543
|
return;
|
|
266
544
|
}
|
|
267
545
|
|
|
268
546
|
if (format === 'compact') {
|
|
269
547
|
const level = entry.level.toUpperCase().padEnd(5);
|
|
270
548
|
const context = entry.context ? `[${entry.context}] ` : '';
|
|
271
|
-
|
|
549
|
+
this.output(`${level} ${context}${entry.message}\n`, entry.level);
|
|
272
550
|
return;
|
|
273
551
|
}
|
|
274
552
|
|
|
@@ -278,30 +556,49 @@ export class MoroLogger implements Logger {
|
|
|
278
556
|
|
|
279
557
|
private writePrettyLog(entry: LogEntry): void {
|
|
280
558
|
const colors = this.options.enableColors !== false;
|
|
281
|
-
const
|
|
559
|
+
const levelReset = colors ? MoroLogger.RESET : '';
|
|
282
560
|
|
|
283
|
-
|
|
561
|
+
MoroLogger.resetStringBuilder();
|
|
562
|
+
|
|
563
|
+
// Timestamp with caching optimization
|
|
284
564
|
if (this.options.enableTimestamp !== false) {
|
|
285
|
-
const timestamp = entry.timestamp
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
565
|
+
const timestamp = this.getCachedTimestamp(entry.timestamp);
|
|
566
|
+
if (colors) {
|
|
567
|
+
MoroLogger.appendToBuilder(MoroLogger.COLORS.timestamp);
|
|
568
|
+
MoroLogger.appendToBuilder(timestamp);
|
|
569
|
+
MoroLogger.appendToBuilder(levelReset);
|
|
570
|
+
} else {
|
|
571
|
+
MoroLogger.appendToBuilder(timestamp);
|
|
572
|
+
}
|
|
573
|
+
MoroLogger.appendToBuilder(' ');
|
|
289
574
|
}
|
|
290
575
|
|
|
291
|
-
// Level with
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
576
|
+
// Level with pre-allocated strings
|
|
577
|
+
const levelStr = MoroLogger.LEVEL_STRINGS[entry.level];
|
|
578
|
+
if (colors) {
|
|
579
|
+
MoroLogger.appendToBuilder(MoroLogger.COLORS[entry.level]);
|
|
580
|
+
MoroLogger.appendToBuilder(MoroLogger.BOLD);
|
|
581
|
+
MoroLogger.appendToBuilder(levelStr);
|
|
582
|
+
MoroLogger.appendToBuilder(levelReset);
|
|
583
|
+
} else {
|
|
584
|
+
MoroLogger.appendToBuilder(levelStr);
|
|
585
|
+
}
|
|
296
586
|
|
|
297
587
|
// Context
|
|
298
588
|
if (entry.context && this.options.enableContext !== false) {
|
|
299
|
-
|
|
300
|
-
|
|
589
|
+
MoroLogger.appendToBuilder(' ');
|
|
590
|
+
if (colors) {
|
|
591
|
+
MoroLogger.appendToBuilder(MoroLogger.COLORS.context);
|
|
592
|
+
MoroLogger.appendToBuilder(`[${entry.context}]`);
|
|
593
|
+
MoroLogger.appendToBuilder(levelReset);
|
|
594
|
+
} else {
|
|
595
|
+
MoroLogger.appendToBuilder(`[${entry.context}]`);
|
|
596
|
+
}
|
|
301
597
|
}
|
|
302
598
|
|
|
303
599
|
// Message
|
|
304
|
-
|
|
600
|
+
MoroLogger.appendToBuilder(' ');
|
|
601
|
+
MoroLogger.appendToBuilder(entry.message);
|
|
305
602
|
|
|
306
603
|
// Performance info
|
|
307
604
|
if (entry.performance && this.options.enablePerformance !== false) {
|
|
@@ -316,32 +613,273 @@ export class MoroLogger implements Logger {
|
|
|
316
613
|
}
|
|
317
614
|
|
|
318
615
|
if (perfParts.length > 0) {
|
|
319
|
-
|
|
616
|
+
MoroLogger.appendToBuilder(' ');
|
|
617
|
+
if (colors) {
|
|
618
|
+
MoroLogger.appendToBuilder(perfColor);
|
|
619
|
+
MoroLogger.appendToBuilder(`(${perfParts.join(', ')})`);
|
|
620
|
+
MoroLogger.appendToBuilder(levelReset);
|
|
621
|
+
} else {
|
|
622
|
+
MoroLogger.appendToBuilder(`(${perfParts.join(', ')})`);
|
|
623
|
+
}
|
|
320
624
|
}
|
|
321
625
|
}
|
|
322
626
|
|
|
323
|
-
// Metadata
|
|
627
|
+
// Metadata with optimized JSON stringify
|
|
324
628
|
if (
|
|
325
629
|
entry.metadata &&
|
|
326
630
|
Object.keys(entry.metadata).length > 0 &&
|
|
327
631
|
this.options.enableMetadata !== false
|
|
328
632
|
) {
|
|
329
633
|
const metaColor = colors ? MoroLogger.COLORS.metadata : '';
|
|
330
|
-
const cleanMetadata =
|
|
331
|
-
delete cleanMetadata.stack; // Handle stack separately
|
|
634
|
+
const cleanMetadata = this.cleanMetadata(entry.metadata);
|
|
332
635
|
|
|
333
636
|
if (Object.keys(cleanMetadata).length > 0) {
|
|
334
|
-
|
|
637
|
+
MoroLogger.appendToBuilder(' ');
|
|
638
|
+
if (colors) {
|
|
639
|
+
MoroLogger.appendToBuilder(metaColor);
|
|
640
|
+
MoroLogger.appendToBuilder(this.safeStringify(cleanMetadata));
|
|
641
|
+
MoroLogger.appendToBuilder(levelReset);
|
|
642
|
+
} else {
|
|
643
|
+
MoroLogger.appendToBuilder(this.safeStringify(cleanMetadata));
|
|
644
|
+
}
|
|
335
645
|
}
|
|
336
646
|
}
|
|
337
647
|
|
|
338
|
-
// Output main log line
|
|
339
|
-
|
|
648
|
+
// Output main log line with high-performance method
|
|
649
|
+
const finalMessage = MoroLogger.buildString();
|
|
650
|
+
this.output(`${finalMessage}\n`, entry.level);
|
|
340
651
|
|
|
341
652
|
// Stack trace for errors
|
|
342
653
|
if (entry.metadata?.stack && (entry.level === 'error' || entry.level === 'fatal')) {
|
|
343
654
|
const stackColor = colors ? MoroLogger.COLORS.error : '';
|
|
344
|
-
|
|
655
|
+
this.output(`${stackColor}${entry.metadata.stack}${levelReset}\n`, entry.level);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Optimized metadata cleaning to avoid unnecessary object operations
|
|
660
|
+
private cleanMetadata(metadata: Record<string, any>): Record<string, any> {
|
|
661
|
+
const clean: Record<string, any> = {};
|
|
662
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
663
|
+
if (key !== 'stack') {
|
|
664
|
+
clean[key] = value;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return clean;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// High-performance output with buffering
|
|
671
|
+
private output(message: string, level: LogLevel = 'info'): void {
|
|
672
|
+
// Prevent memory exhaustion
|
|
673
|
+
if (
|
|
674
|
+
this.outputBuffer.length >= this.bufferOverflowThreshold &&
|
|
675
|
+
!this.emergencyFlushInProgress
|
|
676
|
+
) {
|
|
677
|
+
this.emergencyFlushInProgress = true;
|
|
678
|
+
this.forceFlushBuffer();
|
|
679
|
+
this.emergencyFlushInProgress = false;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
this.outputBuffer.push(message);
|
|
683
|
+
this.bufferSize++;
|
|
684
|
+
|
|
685
|
+
// Immediate flush for critical levels or full buffer
|
|
686
|
+
if (level === 'fatal' || level === 'error' || this.bufferSize >= this.maxBufferSize) {
|
|
687
|
+
this.flushBuffer();
|
|
688
|
+
} else {
|
|
689
|
+
this.scheduleFlush();
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private scheduleFlush(): void {
|
|
694
|
+
if (this.flushTimeout) {
|
|
695
|
+
return; // Already scheduled
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
this.flushTimeout = setTimeout(() => {
|
|
699
|
+
this.flushBuffer();
|
|
700
|
+
}, this.flushInterval);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
private flushBuffer(): void {
|
|
704
|
+
if (this.outputBuffer.length === 0) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Group messages by stream type
|
|
709
|
+
const stdoutMessages: string[] = [];
|
|
710
|
+
const stderrMessages: string[] = [];
|
|
711
|
+
|
|
712
|
+
for (const message of this.outputBuffer) {
|
|
713
|
+
// Determine stream based on message content or level
|
|
714
|
+
if (message.includes('ERROR') || message.includes('FATAL')) {
|
|
715
|
+
stderrMessages.push(message);
|
|
716
|
+
} else {
|
|
717
|
+
stdoutMessages.push(message);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Write to appropriate streams with error handling
|
|
722
|
+
try {
|
|
723
|
+
if (stdoutMessages.length > 0 && process.stdout.writable) {
|
|
724
|
+
process.stdout.write(stdoutMessages.join(''));
|
|
725
|
+
}
|
|
726
|
+
if (stderrMessages.length > 0 && process.stderr.writable) {
|
|
727
|
+
process.stderr.write(stderrMessages.join(''));
|
|
728
|
+
}
|
|
729
|
+
} catch {
|
|
730
|
+
// Fallback to console if streams fail
|
|
731
|
+
try {
|
|
732
|
+
// eslint-disable-next-line no-console
|
|
733
|
+
console.log(this.outputBuffer.join(''));
|
|
734
|
+
} catch {
|
|
735
|
+
// If even console.log fails, just ignore
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Clear buffer
|
|
740
|
+
this.outputBuffer.length = 0;
|
|
741
|
+
this.bufferSize = 0;
|
|
742
|
+
|
|
743
|
+
// Clear timeout
|
|
744
|
+
if (this.flushTimeout) {
|
|
745
|
+
clearTimeout(this.flushTimeout);
|
|
746
|
+
this.flushTimeout = null;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Emergency flush for buffer overflow protection
|
|
751
|
+
private forceFlushBuffer(): void {
|
|
752
|
+
if (this.outputBuffer.length === 0) return;
|
|
753
|
+
|
|
754
|
+
try {
|
|
755
|
+
const message = this.outputBuffer.join('');
|
|
756
|
+
process.stdout.write(message);
|
|
757
|
+
} catch (error) {
|
|
758
|
+
// Emergency fallback - write individual messages
|
|
759
|
+
for (const msg of this.outputBuffer) {
|
|
760
|
+
try {
|
|
761
|
+
process.stdout.write(msg);
|
|
762
|
+
} catch {
|
|
763
|
+
// If even this fails, give up on this batch
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
} finally {
|
|
768
|
+
this.outputBuffer.length = 0;
|
|
769
|
+
this.bufferSize = 0;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Safe stringify with circular reference detection
|
|
774
|
+
private safeStringify(obj: any, maxDepth = 3): string {
|
|
775
|
+
const seen = new WeakSet();
|
|
776
|
+
|
|
777
|
+
const stringify = (value: any, depth: number): any => {
|
|
778
|
+
if (depth > maxDepth) return '[Max Depth Reached]';
|
|
779
|
+
if (value === null || typeof value !== 'object') return value;
|
|
780
|
+
if (seen.has(value)) return '[Circular Reference]';
|
|
781
|
+
|
|
782
|
+
seen.add(value);
|
|
783
|
+
|
|
784
|
+
if (Array.isArray(value)) {
|
|
785
|
+
return value.map(item => stringify(item, depth + 1));
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const result: any = {};
|
|
789
|
+
for (const [key, val] of Object.entries(value)) {
|
|
790
|
+
if (typeof val !== 'function') {
|
|
791
|
+
// Skip functions
|
|
792
|
+
result[key] = stringify(val, depth + 1);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return result;
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
try {
|
|
799
|
+
return JSON.stringify(stringify(obj, 0));
|
|
800
|
+
} catch (error) {
|
|
801
|
+
return '[Stringify Error]';
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Configuration validation
|
|
806
|
+
private validateOptions(options: LoggerOptions): LoggerOptions {
|
|
807
|
+
const validated = { ...options };
|
|
808
|
+
|
|
809
|
+
// Validate log level
|
|
810
|
+
const validLevels = ['debug', 'info', 'warn', 'error', 'fatal'];
|
|
811
|
+
if (validated.level && !validLevels.includes(validated.level)) {
|
|
812
|
+
console.warn(`[MoroLogger] Invalid log level: ${validated.level}, defaulting to 'info'`);
|
|
813
|
+
validated.level = 'info';
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Validate max entries
|
|
817
|
+
if (validated.maxEntries !== undefined) {
|
|
818
|
+
if (validated.maxEntries < 1 || validated.maxEntries > 100000) {
|
|
819
|
+
console.warn(
|
|
820
|
+
`[MoroLogger] Invalid maxEntries: ${validated.maxEntries}, defaulting to 1000`
|
|
821
|
+
);
|
|
822
|
+
validated.maxEntries = 1000;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Validate buffer size
|
|
827
|
+
if (validated.maxBufferSize !== undefined) {
|
|
828
|
+
if (validated.maxBufferSize < 10 || validated.maxBufferSize > 10000) {
|
|
829
|
+
console.warn(
|
|
830
|
+
`[MoroLogger] Invalid maxBufferSize: ${validated.maxBufferSize}, defaulting to 1000`
|
|
831
|
+
);
|
|
832
|
+
validated.maxBufferSize = 1000;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return validated;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Error handling methods
|
|
840
|
+
private handleOutputError(outputName: string, error: any): void {
|
|
841
|
+
// Could implement output retry logic, circuit breaker, etc.
|
|
842
|
+
// For now, just track the error
|
|
843
|
+
if (!this.metrics.outputErrors) {
|
|
844
|
+
this.metrics.outputErrors = {};
|
|
845
|
+
}
|
|
846
|
+
this.metrics.outputErrors[outputName] = (this.metrics.outputErrors[outputName] || 0) + 1;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
private emergencyConsoleWrite(entry: LogEntry): void {
|
|
850
|
+
const message = `${entry.timestamp.toISOString()} ${entry.level.toUpperCase()} ${entry.message}`;
|
|
851
|
+
try {
|
|
852
|
+
if (entry.level === 'error' || entry.level === 'fatal') {
|
|
853
|
+
process.stderr.write(`[EMERGENCY] ${message}\n`);
|
|
854
|
+
} else {
|
|
855
|
+
process.stdout.write(`[EMERGENCY] ${message}\n`);
|
|
856
|
+
}
|
|
857
|
+
} catch {
|
|
858
|
+
// If even emergency write fails, there's nothing more we can do
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Force flush streams (useful for shutdown)
|
|
863
|
+
public flush(): void {
|
|
864
|
+
// Clear any pending flush timeout
|
|
865
|
+
if (this.flushTimeout) {
|
|
866
|
+
clearTimeout(this.flushTimeout);
|
|
867
|
+
this.flushTimeout = null;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Flush any remaining buffer
|
|
871
|
+
this.flushBuffer();
|
|
872
|
+
|
|
873
|
+
try {
|
|
874
|
+
// Force flush streams without ending them
|
|
875
|
+
if (process.stdout.writable) {
|
|
876
|
+
process.stdout.write(''); // Force flush without ending
|
|
877
|
+
}
|
|
878
|
+
if (process.stderr.writable) {
|
|
879
|
+
process.stderr.write(''); // Force flush without ending
|
|
880
|
+
}
|
|
881
|
+
} catch {
|
|
882
|
+
// Ignore flush errors
|
|
345
883
|
}
|
|
346
884
|
}
|
|
347
885
|
}
|
|
@@ -397,3 +935,18 @@ export function applyLoggingConfiguration(
|
|
|
397
935
|
export const createFrameworkLogger = (context: string) => {
|
|
398
936
|
return logger.child('Moro', { framework: 'moro', context });
|
|
399
937
|
};
|
|
938
|
+
|
|
939
|
+
// Graceful shutdown handler to flush any pending logs
|
|
940
|
+
process.on('SIGINT', () => {
|
|
941
|
+
logger.flush();
|
|
942
|
+
process.exit(0);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
process.on('SIGTERM', () => {
|
|
946
|
+
logger.flush();
|
|
947
|
+
process.exit(0);
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
process.on('beforeExit', () => {
|
|
951
|
+
logger.flush();
|
|
952
|
+
});
|