@morojs/moro 1.5.3 → 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.
Files changed (36) hide show
  1. package/dist/core/auth/morojs-adapter.js +23 -12
  2. package/dist/core/auth/morojs-adapter.js.map +1 -1
  3. package/dist/core/http/http-server.js +12 -8
  4. package/dist/core/http/http-server.js.map +1 -1
  5. package/dist/core/logger/filters.js +12 -4
  6. package/dist/core/logger/filters.js.map +1 -1
  7. package/dist/core/logger/logger.d.ts +44 -0
  8. package/dist/core/logger/logger.js +550 -60
  9. package/dist/core/logger/logger.js.map +1 -1
  10. package/dist/core/middleware/built-in/request-logger.js +4 -2
  11. package/dist/core/middleware/built-in/request-logger.js.map +1 -1
  12. package/dist/core/modules/auto-discovery.d.ts +1 -0
  13. package/dist/core/modules/auto-discovery.js +9 -5
  14. package/dist/core/modules/auto-discovery.js.map +1 -1
  15. package/dist/core/modules/modules.d.ts +1 -0
  16. package/dist/core/modules/modules.js +8 -2
  17. package/dist/core/modules/modules.js.map +1 -1
  18. package/dist/core/networking/adapters/ws-adapter.d.ts +1 -0
  19. package/dist/core/networking/adapters/ws-adapter.js +3 -1
  20. package/dist/core/networking/adapters/ws-adapter.js.map +1 -1
  21. package/dist/core/networking/service-discovery.d.ts +1 -0
  22. package/dist/core/networking/service-discovery.js +23 -11
  23. package/dist/core/networking/service-discovery.js.map +1 -1
  24. package/dist/types/logger.d.ts +3 -0
  25. package/package.json +1 -1
  26. package/src/core/auth/morojs-adapter.ts +25 -12
  27. package/src/core/database/README.md +26 -16
  28. package/src/core/http/http-server.ts +15 -12
  29. package/src/core/logger/filters.ts +12 -4
  30. package/src/core/logger/logger.ts +616 -63
  31. package/src/core/middleware/built-in/request-logger.ts +6 -2
  32. package/src/core/modules/auto-discovery.ts +13 -5
  33. package/src/core/modules/modules.ts +9 -5
  34. package/src/core/networking/adapters/ws-adapter.ts +3 -1
  35. package/src/core/networking/service-discovery.ts +23 -9
  36. 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
- const entries = [...this.history];
162
- return count ? entries.slice(-count) : entries;
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
- // Check level threshold - use parent level if available (for child loggers)
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
- // Create log entry
205
- const entry: LogEntry = {
206
- timestamp: new Date(),
207
- level,
208
- message,
209
- context: this.contextPrefix
210
- ? context
211
- ? `${this.contextPrefix}:${context}`
212
- : this.contextPrefix
213
- : context,
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
- // Apply filters
223
- for (const filter of this.filters.values()) {
224
- if (!filter.filter(entry)) {
225
- return;
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.history.push(entry);
234
- if (this.history.length > (this.options.maxEntries || 1000)) {
235
- this.history.shift();
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
- // Write to outputs
239
- for (const output of this.outputs.values()) {
240
- if (!output.level || MoroLogger.LEVELS[level] >= MoroLogger.LEVELS[output.level]) {
241
- try {
242
- output.write(entry);
243
- } catch (error) {
244
- console.error('Logger output error:', error);
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
- console.log(JSON.stringify(entry));
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
- console.log(`${level} ${context}${entry.message}`);
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 parts: string[] = [];
559
+ const levelReset = colors ? MoroLogger.RESET : '';
282
560
 
283
- // Timestamp
561
+ MoroLogger.resetStringBuilder();
562
+
563
+ // Timestamp with caching optimization
284
564
  if (this.options.enableTimestamp !== false) {
285
- const timestamp = entry.timestamp.toISOString().replace('T', ' ').slice(0, 19);
286
- parts.push(
287
- colors ? `${MoroLogger.COLORS.timestamp}${timestamp}${MoroLogger.RESET}` : timestamp
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 color (remove icons)
292
- const levelColor = colors ? MoroLogger.COLORS[entry.level] : '';
293
- const levelReset = colors ? MoroLogger.RESET : '';
294
- const levelText = entry.level.toUpperCase();
295
- parts.push(`${levelColor}${MoroLogger.BOLD}${levelText}${levelReset}`);
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
- const contextColor = colors ? MoroLogger.COLORS.context : '';
300
- parts.push(`${contextColor}[${entry.context}]${levelReset}`);
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
- parts.push(entry.message);
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
- parts.push(`${perfColor}(${perfParts.join(', ')})${levelReset}`);
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 = { ...entry.metadata };
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
- parts.push(`${metaColor}${JSON.stringify(cleanMetadata)}${levelReset}`);
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
- console.log(parts.join(' '));
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
- console.log(`${stackColor}${entry.metadata.stack}${levelReset}`);
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
+ });