@morojs/moro 1.5.3 → 1.5.5
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 +45 -0
- package/dist/core/logger/logger.js +579 -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/moro.d.ts +35 -0
- package/dist/moro.js +156 -25
- package/dist/moro.js.map +1 -1
- package/dist/types/logger.d.ts +3 -0
- package/package.json +1 -1
- 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 +649 -62
- 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/moro.ts +200 -28
- 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,62 +329,165 @@ 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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
345
|
+
// Absolute minimal path for simple logs - pure speed
|
|
346
|
+
if (!metadata && !context && !this.contextPrefix && !this.contextMetadata) {
|
|
347
|
+
this.writeSimpleLog(level, message);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Minimal path for logs with context but no metadata
|
|
352
|
+
if (!metadata && !this.contextMetadata) {
|
|
353
|
+
this.writeSimpleLog(level, message, context);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Path for complex logs
|
|
358
|
+
if (metadata && Object.keys(metadata).length > 0) {
|
|
359
|
+
this.complexLog(level, message, context, metadata);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Full logging path for complex logs
|
|
364
|
+
this.fullLog(level, message, context, metadata);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Full logging with all features
|
|
368
|
+
private fullLog(
|
|
369
|
+
level: LogLevel,
|
|
370
|
+
message: string,
|
|
371
|
+
context?: string,
|
|
372
|
+
metadata?: Record<string, any>
|
|
373
|
+
): void {
|
|
374
|
+
// Use object pooling for LogEntry (Pino's technique)
|
|
375
|
+
const entry = MoroLogger.getPooledEntry();
|
|
376
|
+
const now = Date.now();
|
|
221
377
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
378
|
+
entry.timestamp = new Date(now);
|
|
379
|
+
entry.level = level;
|
|
380
|
+
entry.message = message;
|
|
381
|
+
entry.context = this.contextPrefix
|
|
382
|
+
? context
|
|
383
|
+
? `${this.contextPrefix}:${context}`
|
|
384
|
+
: this.contextPrefix
|
|
385
|
+
: context;
|
|
386
|
+
entry.metadata = this.createMetadata(metadata);
|
|
387
|
+
entry.performance = this.options.enablePerformance ? this.getPerformanceData(now) : undefined;
|
|
388
|
+
|
|
389
|
+
// Apply filters with early return optimization
|
|
390
|
+
if (this.filters.size > 0) {
|
|
391
|
+
for (const filter of this.filters.values()) {
|
|
392
|
+
if (!filter.filter(entry)) {
|
|
393
|
+
MoroLogger.returnPooledEntry(entry);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
226
396
|
}
|
|
227
397
|
}
|
|
228
398
|
|
|
229
399
|
// Update metrics
|
|
230
400
|
this.updateMetrics(entry);
|
|
231
401
|
|
|
232
|
-
// Store in history
|
|
233
|
-
this.
|
|
234
|
-
|
|
235
|
-
|
|
402
|
+
// Store in history with circular buffer optimization
|
|
403
|
+
this.addToHistory(entry);
|
|
404
|
+
|
|
405
|
+
// Write to outputs with batched processing
|
|
406
|
+
this.writeToOutputs(entry, level);
|
|
407
|
+
|
|
408
|
+
// Return entry to pool
|
|
409
|
+
MoroLogger.returnPooledEntry(entry);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Absolute minimal logging - pure speed, no overhead
|
|
413
|
+
private complexLog(
|
|
414
|
+
level: LogLevel,
|
|
415
|
+
message: string,
|
|
416
|
+
context?: string,
|
|
417
|
+
metadata?: Record<string, any>
|
|
418
|
+
): void {
|
|
419
|
+
// Use object pooling for LogEntry (Pino's technique)
|
|
420
|
+
const entry = MoroLogger.getPooledEntry();
|
|
421
|
+
const now = Date.now();
|
|
422
|
+
|
|
423
|
+
entry.timestamp = new Date(now);
|
|
424
|
+
entry.level = level;
|
|
425
|
+
entry.message = message;
|
|
426
|
+
entry.context = this.contextPrefix
|
|
427
|
+
? context
|
|
428
|
+
? `${this.contextPrefix}:${context}`
|
|
429
|
+
: this.contextPrefix
|
|
430
|
+
: context;
|
|
431
|
+
entry.metadata = this.createMetadata(metadata);
|
|
432
|
+
entry.performance = this.options.enablePerformance ? this.getPerformanceData(now) : undefined;
|
|
433
|
+
|
|
434
|
+
// Write to outputs with batched processing
|
|
435
|
+
this.writeToOutputs(entry, level);
|
|
436
|
+
|
|
437
|
+
// Return entry to pool
|
|
438
|
+
MoroLogger.returnPooledEntry(entry);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Simple log writer with colors for minimal overhead cases
|
|
442
|
+
private writeSimpleLog(level: LogLevel, message: string, context?: string): void {
|
|
443
|
+
const colors = this.options.enableColors !== false;
|
|
444
|
+
const levelReset = colors ? MoroLogger.RESET : '';
|
|
445
|
+
|
|
446
|
+
MoroLogger.resetStringBuilder();
|
|
447
|
+
|
|
448
|
+
// Timestamp with caching optimization
|
|
449
|
+
if (this.options.enableTimestamp !== false) {
|
|
450
|
+
const timestamp = this.getFastCachedTimestamp();
|
|
451
|
+
if (colors) {
|
|
452
|
+
MoroLogger.appendToBuilder(MoroLogger.COLORS.timestamp);
|
|
453
|
+
MoroLogger.appendToBuilder(timestamp);
|
|
454
|
+
MoroLogger.appendToBuilder(levelReset);
|
|
455
|
+
} else {
|
|
456
|
+
MoroLogger.appendToBuilder(timestamp);
|
|
457
|
+
}
|
|
458
|
+
MoroLogger.appendToBuilder(' ');
|
|
236
459
|
}
|
|
237
460
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
461
|
+
// Level with pre-allocated strings
|
|
462
|
+
const levelStr = MoroLogger.LEVEL_STRINGS[level];
|
|
463
|
+
if (colors) {
|
|
464
|
+
MoroLogger.appendToBuilder(MoroLogger.COLORS[level]);
|
|
465
|
+
MoroLogger.appendToBuilder(MoroLogger.BOLD);
|
|
466
|
+
MoroLogger.appendToBuilder(levelStr);
|
|
467
|
+
MoroLogger.appendToBuilder(levelReset);
|
|
468
|
+
} else {
|
|
469
|
+
MoroLogger.appendToBuilder(levelStr);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Context
|
|
473
|
+
if (context && this.options.enableContext !== false) {
|
|
474
|
+
MoroLogger.appendToBuilder(' ');
|
|
475
|
+
if (colors) {
|
|
476
|
+
MoroLogger.appendToBuilder(MoroLogger.COLORS.context);
|
|
477
|
+
MoroLogger.appendToBuilder(`[${context}]`);
|
|
478
|
+
MoroLogger.appendToBuilder(levelReset);
|
|
479
|
+
} else {
|
|
480
|
+
MoroLogger.appendToBuilder(`[${context}]`);
|
|
246
481
|
}
|
|
247
482
|
}
|
|
483
|
+
|
|
484
|
+
// Message
|
|
485
|
+
MoroLogger.appendToBuilder(' ');
|
|
486
|
+
MoroLogger.appendToBuilder(message);
|
|
487
|
+
|
|
488
|
+
// Output main log line with high-performance method
|
|
489
|
+
const finalMessage = MoroLogger.buildString();
|
|
490
|
+
this.output(`${finalMessage}\n`, level);
|
|
248
491
|
}
|
|
249
492
|
|
|
250
493
|
private updateMetrics(entry: LogEntry): void {
|
|
@@ -257,18 +500,87 @@ export class MoroLogger implements Logger {
|
|
|
257
500
|
}
|
|
258
501
|
}
|
|
259
502
|
|
|
503
|
+
// Optimized metadata creation to avoid unnecessary object spreading
|
|
504
|
+
private createMetadata(metadata?: Record<string, any>): Record<string, any> {
|
|
505
|
+
if (!metadata && !this.contextMetadata) {
|
|
506
|
+
return {};
|
|
507
|
+
}
|
|
508
|
+
if (!metadata) {
|
|
509
|
+
return { ...this.contextMetadata };
|
|
510
|
+
}
|
|
511
|
+
if (!this.contextMetadata) {
|
|
512
|
+
return { ...metadata };
|
|
513
|
+
}
|
|
514
|
+
return { ...this.contextMetadata, ...metadata };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Optimized performance data with caching
|
|
518
|
+
private getPerformanceData(now: number): { memory: number } | undefined {
|
|
519
|
+
if (now - this.lastMemoryCheck > this.memoryCheckInterval) {
|
|
520
|
+
this.lastMemoryCheck = now;
|
|
521
|
+
this.metrics.memoryUsage = process.memoryUsage().heapUsed / 1024 / 1024;
|
|
522
|
+
}
|
|
523
|
+
return { memory: this.metrics.memoryUsage };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Circular buffer implementation for history (O(1) instead of O(n))
|
|
527
|
+
private addToHistory(entry: LogEntry): void {
|
|
528
|
+
const maxEntries = this.options.maxEntries || 1000;
|
|
529
|
+
|
|
530
|
+
if (this.historySize < maxEntries) {
|
|
531
|
+
this.history[this.historySize] = entry;
|
|
532
|
+
this.historySize++;
|
|
533
|
+
} else {
|
|
534
|
+
// Circular buffer: overwrite oldest entry
|
|
535
|
+
this.history[this.historyIndex] = entry;
|
|
536
|
+
this.historyIndex = (this.historyIndex + 1) % maxEntries;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Optimized output writing with batching
|
|
541
|
+
private writeToOutputs(entry: LogEntry, level: LogLevel): void {
|
|
542
|
+
if (this.outputs.size === 0) return;
|
|
543
|
+
|
|
544
|
+
let successCount = 0;
|
|
545
|
+
const errors: Array<{ outputName: string; error: any }> = [];
|
|
546
|
+
|
|
547
|
+
for (const output of this.outputs.values()) {
|
|
548
|
+
if (!output.level || MoroLogger.LEVELS[level] >= MoroLogger.LEVELS[output.level]) {
|
|
549
|
+
try {
|
|
550
|
+
output.write(entry);
|
|
551
|
+
successCount++;
|
|
552
|
+
} catch (error) {
|
|
553
|
+
errors.push({ outputName: output.name, error });
|
|
554
|
+
this.handleOutputError(output.name, error);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// If all outputs fail, use emergency console
|
|
560
|
+
if (successCount === 0 && this.outputs.size > 0) {
|
|
561
|
+
this.emergencyConsoleWrite(entry);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Log output errors (but avoid infinite loops)
|
|
565
|
+
if (errors.length > 0 && level !== 'error') {
|
|
566
|
+
this.error(`Logger output errors: ${errors.length} failed`, 'MoroLogger', {
|
|
567
|
+
errors: errors.map(e => e.outputName),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
260
572
|
private writeToConsole(entry: LogEntry): void {
|
|
261
573
|
const format = this.options.format || 'pretty';
|
|
262
574
|
|
|
263
575
|
if (format === 'json') {
|
|
264
|
-
|
|
576
|
+
this.output(`${this.safeStringify(entry)}\n`, entry.level);
|
|
265
577
|
return;
|
|
266
578
|
}
|
|
267
579
|
|
|
268
580
|
if (format === 'compact') {
|
|
269
581
|
const level = entry.level.toUpperCase().padEnd(5);
|
|
270
582
|
const context = entry.context ? `[${entry.context}] ` : '';
|
|
271
|
-
|
|
583
|
+
this.output(`${level} ${context}${entry.message}\n`, entry.level);
|
|
272
584
|
return;
|
|
273
585
|
}
|
|
274
586
|
|
|
@@ -278,30 +590,49 @@ export class MoroLogger implements Logger {
|
|
|
278
590
|
|
|
279
591
|
private writePrettyLog(entry: LogEntry): void {
|
|
280
592
|
const colors = this.options.enableColors !== false;
|
|
281
|
-
const
|
|
593
|
+
const levelReset = colors ? MoroLogger.RESET : '';
|
|
282
594
|
|
|
283
|
-
|
|
595
|
+
MoroLogger.resetStringBuilder();
|
|
596
|
+
|
|
597
|
+
// Timestamp with caching optimization
|
|
284
598
|
if (this.options.enableTimestamp !== false) {
|
|
285
|
-
const timestamp = entry.timestamp
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
599
|
+
const timestamp = this.getCachedTimestamp(entry.timestamp);
|
|
600
|
+
if (colors) {
|
|
601
|
+
MoroLogger.appendToBuilder(MoroLogger.COLORS.timestamp);
|
|
602
|
+
MoroLogger.appendToBuilder(timestamp);
|
|
603
|
+
MoroLogger.appendToBuilder(levelReset);
|
|
604
|
+
} else {
|
|
605
|
+
MoroLogger.appendToBuilder(timestamp);
|
|
606
|
+
}
|
|
607
|
+
MoroLogger.appendToBuilder(' ');
|
|
289
608
|
}
|
|
290
609
|
|
|
291
|
-
// Level with
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
610
|
+
// Level with pre-allocated strings
|
|
611
|
+
const levelStr = MoroLogger.LEVEL_STRINGS[entry.level];
|
|
612
|
+
if (colors) {
|
|
613
|
+
MoroLogger.appendToBuilder(MoroLogger.COLORS[entry.level]);
|
|
614
|
+
MoroLogger.appendToBuilder(MoroLogger.BOLD);
|
|
615
|
+
MoroLogger.appendToBuilder(levelStr);
|
|
616
|
+
MoroLogger.appendToBuilder(levelReset);
|
|
617
|
+
} else {
|
|
618
|
+
MoroLogger.appendToBuilder(levelStr);
|
|
619
|
+
}
|
|
296
620
|
|
|
297
621
|
// Context
|
|
298
622
|
if (entry.context && this.options.enableContext !== false) {
|
|
299
|
-
|
|
300
|
-
|
|
623
|
+
MoroLogger.appendToBuilder(' ');
|
|
624
|
+
if (colors) {
|
|
625
|
+
MoroLogger.appendToBuilder(MoroLogger.COLORS.context);
|
|
626
|
+
MoroLogger.appendToBuilder(`[${entry.context}]`);
|
|
627
|
+
MoroLogger.appendToBuilder(levelReset);
|
|
628
|
+
} else {
|
|
629
|
+
MoroLogger.appendToBuilder(`[${entry.context}]`);
|
|
630
|
+
}
|
|
301
631
|
}
|
|
302
632
|
|
|
303
633
|
// Message
|
|
304
|
-
|
|
634
|
+
MoroLogger.appendToBuilder(' ');
|
|
635
|
+
MoroLogger.appendToBuilder(entry.message);
|
|
305
636
|
|
|
306
637
|
// Performance info
|
|
307
638
|
if (entry.performance && this.options.enablePerformance !== false) {
|
|
@@ -316,32 +647,273 @@ export class MoroLogger implements Logger {
|
|
|
316
647
|
}
|
|
317
648
|
|
|
318
649
|
if (perfParts.length > 0) {
|
|
319
|
-
|
|
650
|
+
MoroLogger.appendToBuilder(' ');
|
|
651
|
+
if (colors) {
|
|
652
|
+
MoroLogger.appendToBuilder(perfColor);
|
|
653
|
+
MoroLogger.appendToBuilder(`(${perfParts.join(', ')})`);
|
|
654
|
+
MoroLogger.appendToBuilder(levelReset);
|
|
655
|
+
} else {
|
|
656
|
+
MoroLogger.appendToBuilder(`(${perfParts.join(', ')})`);
|
|
657
|
+
}
|
|
320
658
|
}
|
|
321
659
|
}
|
|
322
660
|
|
|
323
|
-
// Metadata
|
|
661
|
+
// Metadata with optimized JSON stringify
|
|
324
662
|
if (
|
|
325
663
|
entry.metadata &&
|
|
326
664
|
Object.keys(entry.metadata).length > 0 &&
|
|
327
665
|
this.options.enableMetadata !== false
|
|
328
666
|
) {
|
|
329
667
|
const metaColor = colors ? MoroLogger.COLORS.metadata : '';
|
|
330
|
-
const cleanMetadata =
|
|
331
|
-
delete cleanMetadata.stack; // Handle stack separately
|
|
668
|
+
const cleanMetadata = this.cleanMetadata(entry.metadata);
|
|
332
669
|
|
|
333
670
|
if (Object.keys(cleanMetadata).length > 0) {
|
|
334
|
-
|
|
671
|
+
MoroLogger.appendToBuilder(' ');
|
|
672
|
+
if (colors) {
|
|
673
|
+
MoroLogger.appendToBuilder(metaColor);
|
|
674
|
+
MoroLogger.appendToBuilder(this.safeStringify(cleanMetadata));
|
|
675
|
+
MoroLogger.appendToBuilder(levelReset);
|
|
676
|
+
} else {
|
|
677
|
+
MoroLogger.appendToBuilder(this.safeStringify(cleanMetadata));
|
|
678
|
+
}
|
|
335
679
|
}
|
|
336
680
|
}
|
|
337
681
|
|
|
338
|
-
// Output main log line
|
|
339
|
-
|
|
682
|
+
// Output main log line with high-performance method
|
|
683
|
+
const finalMessage = MoroLogger.buildString();
|
|
684
|
+
this.output(`${finalMessage}\n`, entry.level);
|
|
340
685
|
|
|
341
686
|
// Stack trace for errors
|
|
342
687
|
if (entry.metadata?.stack && (entry.level === 'error' || entry.level === 'fatal')) {
|
|
343
688
|
const stackColor = colors ? MoroLogger.COLORS.error : '';
|
|
344
|
-
|
|
689
|
+
this.output(`${stackColor}${entry.metadata.stack}${levelReset}\n`, entry.level);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Optimized metadata cleaning to avoid unnecessary object operations
|
|
694
|
+
private cleanMetadata(metadata: Record<string, any>): Record<string, any> {
|
|
695
|
+
const clean: Record<string, any> = {};
|
|
696
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
697
|
+
if (key !== 'stack') {
|
|
698
|
+
clean[key] = value;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return clean;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// High-performance output with buffering
|
|
705
|
+
private output(message: string, level: LogLevel = 'info'): void {
|
|
706
|
+
// Prevent memory exhaustion
|
|
707
|
+
if (
|
|
708
|
+
this.outputBuffer.length >= this.bufferOverflowThreshold &&
|
|
709
|
+
!this.emergencyFlushInProgress
|
|
710
|
+
) {
|
|
711
|
+
this.emergencyFlushInProgress = true;
|
|
712
|
+
this.forceFlushBuffer();
|
|
713
|
+
this.emergencyFlushInProgress = false;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
this.outputBuffer.push(message);
|
|
717
|
+
this.bufferSize++;
|
|
718
|
+
|
|
719
|
+
// Immediate flush for critical levels or full buffer
|
|
720
|
+
if (level === 'fatal' || level === 'error' || this.bufferSize >= this.maxBufferSize) {
|
|
721
|
+
this.flushBuffer();
|
|
722
|
+
} else {
|
|
723
|
+
this.scheduleFlush();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
private scheduleFlush(): void {
|
|
728
|
+
if (this.flushTimeout) {
|
|
729
|
+
return; // Already scheduled
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
this.flushTimeout = setTimeout(() => {
|
|
733
|
+
this.flushBuffer();
|
|
734
|
+
}, this.flushInterval);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
private flushBuffer(): void {
|
|
738
|
+
if (this.outputBuffer.length === 0) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Group messages by stream type
|
|
743
|
+
const stdoutMessages: string[] = [];
|
|
744
|
+
const stderrMessages: string[] = [];
|
|
745
|
+
|
|
746
|
+
for (const message of this.outputBuffer) {
|
|
747
|
+
// Determine stream based on message content or level
|
|
748
|
+
if (message.includes('ERROR') || message.includes('FATAL')) {
|
|
749
|
+
stderrMessages.push(message);
|
|
750
|
+
} else {
|
|
751
|
+
stdoutMessages.push(message);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Write to appropriate streams with error handling
|
|
756
|
+
try {
|
|
757
|
+
if (stdoutMessages.length > 0 && process.stdout.writable) {
|
|
758
|
+
process.stdout.write(stdoutMessages.join(''));
|
|
759
|
+
}
|
|
760
|
+
if (stderrMessages.length > 0 && process.stderr.writable) {
|
|
761
|
+
process.stderr.write(stderrMessages.join(''));
|
|
762
|
+
}
|
|
763
|
+
} catch {
|
|
764
|
+
// Fallback to console if streams fail
|
|
765
|
+
try {
|
|
766
|
+
// eslint-disable-next-line no-console
|
|
767
|
+
console.log(this.outputBuffer.join(''));
|
|
768
|
+
} catch {
|
|
769
|
+
// If even console.log fails, just ignore
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Clear buffer
|
|
774
|
+
this.outputBuffer.length = 0;
|
|
775
|
+
this.bufferSize = 0;
|
|
776
|
+
|
|
777
|
+
// Clear timeout
|
|
778
|
+
if (this.flushTimeout) {
|
|
779
|
+
clearTimeout(this.flushTimeout);
|
|
780
|
+
this.flushTimeout = null;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Emergency flush for buffer overflow protection
|
|
785
|
+
private forceFlushBuffer(): void {
|
|
786
|
+
if (this.outputBuffer.length === 0) return;
|
|
787
|
+
|
|
788
|
+
try {
|
|
789
|
+
const message = this.outputBuffer.join('');
|
|
790
|
+
process.stdout.write(message);
|
|
791
|
+
} catch (error) {
|
|
792
|
+
// Emergency fallback - write individual messages
|
|
793
|
+
for (const msg of this.outputBuffer) {
|
|
794
|
+
try {
|
|
795
|
+
process.stdout.write(msg);
|
|
796
|
+
} catch {
|
|
797
|
+
// If even this fails, give up on this batch
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
} finally {
|
|
802
|
+
this.outputBuffer.length = 0;
|
|
803
|
+
this.bufferSize = 0;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Safe stringify with circular reference detection
|
|
808
|
+
private safeStringify(obj: any, maxDepth = 3): string {
|
|
809
|
+
const seen = new WeakSet();
|
|
810
|
+
|
|
811
|
+
const stringify = (value: any, depth: number): any => {
|
|
812
|
+
if (depth > maxDepth) return '[Max Depth Reached]';
|
|
813
|
+
if (value === null || typeof value !== 'object') return value;
|
|
814
|
+
if (seen.has(value)) return '[Circular Reference]';
|
|
815
|
+
|
|
816
|
+
seen.add(value);
|
|
817
|
+
|
|
818
|
+
if (Array.isArray(value)) {
|
|
819
|
+
return value.map(item => stringify(item, depth + 1));
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const result: any = {};
|
|
823
|
+
for (const [key, val] of Object.entries(value)) {
|
|
824
|
+
if (typeof val !== 'function') {
|
|
825
|
+
// Skip functions
|
|
826
|
+
result[key] = stringify(val, depth + 1);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return result;
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
try {
|
|
833
|
+
return JSON.stringify(stringify(obj, 0));
|
|
834
|
+
} catch (error) {
|
|
835
|
+
return '[Stringify Error]';
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Configuration validation
|
|
840
|
+
private validateOptions(options: LoggerOptions): LoggerOptions {
|
|
841
|
+
const validated = { ...options };
|
|
842
|
+
|
|
843
|
+
// Validate log level
|
|
844
|
+
const validLevels = ['debug', 'info', 'warn', 'error', 'fatal'];
|
|
845
|
+
if (validated.level && !validLevels.includes(validated.level)) {
|
|
846
|
+
console.warn(`[MoroLogger] Invalid log level: ${validated.level}, defaulting to 'info'`);
|
|
847
|
+
validated.level = 'info';
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Validate max entries
|
|
851
|
+
if (validated.maxEntries !== undefined) {
|
|
852
|
+
if (validated.maxEntries < 1 || validated.maxEntries > 100000) {
|
|
853
|
+
console.warn(
|
|
854
|
+
`[MoroLogger] Invalid maxEntries: ${validated.maxEntries}, defaulting to 1000`
|
|
855
|
+
);
|
|
856
|
+
validated.maxEntries = 1000;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Validate buffer size
|
|
861
|
+
if (validated.maxBufferSize !== undefined) {
|
|
862
|
+
if (validated.maxBufferSize < 10 || validated.maxBufferSize > 10000) {
|
|
863
|
+
console.warn(
|
|
864
|
+
`[MoroLogger] Invalid maxBufferSize: ${validated.maxBufferSize}, defaulting to 1000`
|
|
865
|
+
);
|
|
866
|
+
validated.maxBufferSize = 1000;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return validated;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Error handling methods
|
|
874
|
+
private handleOutputError(outputName: string, error: any): void {
|
|
875
|
+
// Could implement output retry logic, circuit breaker, etc.
|
|
876
|
+
// For now, just track the error
|
|
877
|
+
if (!this.metrics.outputErrors) {
|
|
878
|
+
this.metrics.outputErrors = {};
|
|
879
|
+
}
|
|
880
|
+
this.metrics.outputErrors[outputName] = (this.metrics.outputErrors[outputName] || 0) + 1;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
private emergencyConsoleWrite(entry: LogEntry): void {
|
|
884
|
+
const message = `${entry.timestamp.toISOString()} ${entry.level.toUpperCase()} ${entry.message}`;
|
|
885
|
+
try {
|
|
886
|
+
if (entry.level === 'error' || entry.level === 'fatal') {
|
|
887
|
+
process.stderr.write(`[EMERGENCY] ${message}\n`);
|
|
888
|
+
} else {
|
|
889
|
+
process.stdout.write(`[EMERGENCY] ${message}\n`);
|
|
890
|
+
}
|
|
891
|
+
} catch {
|
|
892
|
+
// If even emergency write fails, there's nothing more we can do
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Force flush streams (useful for shutdown)
|
|
897
|
+
public flush(): void {
|
|
898
|
+
// Clear any pending flush timeout
|
|
899
|
+
if (this.flushTimeout) {
|
|
900
|
+
clearTimeout(this.flushTimeout);
|
|
901
|
+
this.flushTimeout = null;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Flush any remaining buffer
|
|
905
|
+
this.flushBuffer();
|
|
906
|
+
|
|
907
|
+
try {
|
|
908
|
+
// Force flush streams without ending them
|
|
909
|
+
if (process.stdout.writable) {
|
|
910
|
+
process.stdout.write(''); // Force flush without ending
|
|
911
|
+
}
|
|
912
|
+
if (process.stderr.writable) {
|
|
913
|
+
process.stderr.write(''); // Force flush without ending
|
|
914
|
+
}
|
|
915
|
+
} catch {
|
|
916
|
+
// Ignore flush errors
|
|
345
917
|
}
|
|
346
918
|
}
|
|
347
919
|
}
|
|
@@ -397,3 +969,18 @@ export function applyLoggingConfiguration(
|
|
|
397
969
|
export const createFrameworkLogger = (context: string) => {
|
|
398
970
|
return logger.child('Moro', { framework: 'moro', context });
|
|
399
971
|
};
|
|
972
|
+
|
|
973
|
+
// Graceful shutdown handler to flush any pending logs
|
|
974
|
+
process.on('SIGINT', () => {
|
|
975
|
+
logger.flush();
|
|
976
|
+
process.exit(0);
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
process.on('SIGTERM', () => {
|
|
980
|
+
logger.flush();
|
|
981
|
+
process.exit(0);
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
process.on('beforeExit', () => {
|
|
985
|
+
logger.flush();
|
|
986
|
+
});
|