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