@peers-app/peers-sdk 0.7.17 → 0.7.19
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/device/connection.test.js +18 -11
- package/dist/device/get-trust-level.js +15 -3
- package/dist/device/streamed-socket.d.ts +0 -7
- package/dist/device/streamed-socket.js +72 -12
- package/dist/device/streamed-socket.test.js +177 -5
- package/dist/device/tx-encoding.d.ts +2 -0
- package/dist/device/tx-encoding.js +44 -0
- package/dist/device/tx-encoding.test.d.ts +1 -0
- package/dist/device/tx-encoding.test.js +267 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/keys.js +10 -11
- package/dist/logging/console-logger.d.ts +53 -0
- package/dist/logging/console-logger.js +272 -1
- package/dist/logging/console-logger.test.d.ts +1 -0
- package/dist/logging/console-logger.test.js +300 -0
- package/dist/logging/console-logs.table.d.ts +2 -2
- package/dist/logging/logger-example.d.ts +41 -0
- package/dist/logging/logger-example.js +74 -0
- package/dist/types/peer-device.d.ts +2 -2
- package/package.json +3 -1
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Logger = void 0;
|
|
3
4
|
exports.setupConsoleProxy = setupConsoleProxy;
|
|
4
5
|
exports.restoreConsole = restoreConsole;
|
|
6
|
+
exports.extractCoreMessage = extractCoreMessage;
|
|
7
|
+
exports.markLoggingInitialized = markLoggingInitialized;
|
|
8
|
+
exports.resetLoggingState = resetLoggingState;
|
|
5
9
|
const utils_1 = require("../utils");
|
|
6
10
|
const console_logs_table_1 = require("./console-logs.table");
|
|
11
|
+
const startupDelayForWritingLogsToDB = Date.now() + 2_000;
|
|
7
12
|
// Store original console methods
|
|
8
13
|
const originalConsole = {
|
|
9
14
|
debug: console.debug,
|
|
@@ -15,6 +20,19 @@ const originalConsole = {
|
|
|
15
20
|
let isProxySetup = false;
|
|
16
21
|
let currentProcessName = 'unknown';
|
|
17
22
|
let currentProcessInstanceId = '';
|
|
23
|
+
// Guard against infinite recursion during log processing (single process)
|
|
24
|
+
let isWritingLog = false;
|
|
25
|
+
let recursionAttempts = 0;
|
|
26
|
+
const MAX_RECURSION_ATTEMPTS = 3;
|
|
27
|
+
// Guard against cross-process infinite loops
|
|
28
|
+
// Track recent log signatures (hash of message + level + source)
|
|
29
|
+
const recentLogSignatures = new Map();
|
|
30
|
+
const LOG_SIGNATURE_WINDOW_MS = 5000; // 5 second window
|
|
31
|
+
const MAX_IDENTICAL_LOGS_IN_WINDOW = 10; // Max same log in window
|
|
32
|
+
// Track initialization state
|
|
33
|
+
let isInitialized = false;
|
|
34
|
+
const pendingLogs = [];
|
|
35
|
+
const MAX_PENDING_LOGS = 1000; // Prevent memory buildup if initialization never happens
|
|
18
36
|
/**
|
|
19
37
|
* Setup console proxy to capture all console output and write to ConsoleLogs table.
|
|
20
38
|
* Also subscribes to dataChanged events to output logs from other process instances.
|
|
@@ -96,6 +114,26 @@ function restoreConsole() {
|
|
|
96
114
|
* Write a log entry to the ConsoleLogs table
|
|
97
115
|
*/
|
|
98
116
|
async function writeLogToDatabase(level, args) {
|
|
117
|
+
const timestamp = (0, utils_1.getTimestamp)();
|
|
118
|
+
if (timestamp < startupDelayForWritingLogsToDB) {
|
|
119
|
+
await (0, utils_1.sleep)(startupDelayForWritingLogsToDB - timestamp);
|
|
120
|
+
}
|
|
121
|
+
// Guard against infinite recursion (same process)
|
|
122
|
+
if (isWritingLog) {
|
|
123
|
+
recursionAttempts++;
|
|
124
|
+
if (recursionAttempts >= MAX_RECURSION_ATTEMPTS) {
|
|
125
|
+
// Break the loop by using original console
|
|
126
|
+
originalConsole.error(`[console-logger] Infinite loop detected after ${recursionAttempts} attempts, dropping log write`, { level, args });
|
|
127
|
+
// Reset counter after a delay to allow recovery
|
|
128
|
+
setTimeout(() => {
|
|
129
|
+
recursionAttempts = 0;
|
|
130
|
+
}, 1000);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Allow a few attempts in case of legitimate nested logging
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
isWritingLog = true;
|
|
99
137
|
try {
|
|
100
138
|
// Format message from arguments
|
|
101
139
|
const message = formatLogMessage(args);
|
|
@@ -105,9 +143,19 @@ async function writeLogToDatabase(level, args) {
|
|
|
105
143
|
const stackTrace = level === 'error' ? getStackTrace() : undefined;
|
|
106
144
|
// Extract source from stack trace
|
|
107
145
|
const source = extractSource(stackTrace);
|
|
146
|
+
// Guard against cross-process infinite loops
|
|
147
|
+
// Extract core message to detect loops even when processes add prefixes
|
|
148
|
+
const coreMessage = extractCoreMessage(message);
|
|
149
|
+
// Create signature from level + source + core message (not the full message)
|
|
150
|
+
const signature = `${level}:${source || 'unknown'}:${coreMessage.substring(0, 100)}`;
|
|
151
|
+
if (!checkLogSignature(signature)) {
|
|
152
|
+
// Too many identical logs in window, likely an infinite loop
|
|
153
|
+
originalConsole.warn(`[console-logger] Cross-process infinite loop detected for log: ${signature.substring(0, 80)}...`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
108
156
|
const logRecord = {
|
|
109
157
|
logId: (0, utils_1.newid)(),
|
|
110
|
-
timestamp
|
|
158
|
+
timestamp,
|
|
111
159
|
level,
|
|
112
160
|
process: currentProcessName,
|
|
113
161
|
processInstanceId: currentProcessInstanceId,
|
|
@@ -120,9 +168,63 @@ async function writeLogToDatabase(level, args) {
|
|
|
120
168
|
await consoleLogsTable.insert(logRecord);
|
|
121
169
|
}
|
|
122
170
|
catch (err) {
|
|
171
|
+
originalConsole.error(`error while trying to write console log`, err);
|
|
123
172
|
// Silently fail if table not available (e.g., during initialization)
|
|
124
173
|
// Don't use console here to avoid infinite recursion
|
|
125
174
|
}
|
|
175
|
+
finally {
|
|
176
|
+
isWritingLog = false;
|
|
177
|
+
recursionAttempts = 0;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Extract core message by removing common prefixes that might be added by different processes
|
|
182
|
+
* This helps detect cross-process loops where each process adds its own prefix
|
|
183
|
+
* @internal Exported for testing
|
|
184
|
+
*/
|
|
185
|
+
function extractCoreMessage(message) {
|
|
186
|
+
let core = message;
|
|
187
|
+
// Remove common process/context prefixes like [main], [renderer], [process-name]
|
|
188
|
+
// Keep removing until no more matches found
|
|
189
|
+
let previousCore = '';
|
|
190
|
+
while (core !== previousCore) {
|
|
191
|
+
previousCore = core;
|
|
192
|
+
// Remove [xxx] prefixes
|
|
193
|
+
core = core.replace(/^\[[^\]]+\]\s*/g, '');
|
|
194
|
+
// Remove common timestamp patterns
|
|
195
|
+
core = core.replace(/^\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[^\s]*\s*/g, '');
|
|
196
|
+
// Remove "Error: " prefix that might be added
|
|
197
|
+
core = core.replace(/^Error:\s*/i, '');
|
|
198
|
+
}
|
|
199
|
+
return core.trim();
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Check if a log signature is within acceptable frequency limits
|
|
203
|
+
* Returns false if this log should be dropped due to suspected infinite loop
|
|
204
|
+
*/
|
|
205
|
+
function checkLogSignature(signature) {
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
// Clean up old entries outside the window
|
|
208
|
+
for (const [key, value] of recentLogSignatures.entries()) {
|
|
209
|
+
if (now - value.firstSeen > LOG_SIGNATURE_WINDOW_MS) {
|
|
210
|
+
recentLogSignatures.delete(key);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Check current signature
|
|
214
|
+
const existing = recentLogSignatures.get(signature);
|
|
215
|
+
if (!existing) {
|
|
216
|
+
// First time seeing this log
|
|
217
|
+
recentLogSignatures.set(signature, { count: 1, firstSeen: now });
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
// Within window, increment count
|
|
221
|
+
existing.count++;
|
|
222
|
+
if (existing.count > MAX_IDENTICAL_LOGS_IN_WINDOW) {
|
|
223
|
+
// Too many identical logs, likely infinite loop
|
|
224
|
+
// Keep the entry to continue blocking (will be cleaned up after window expires)
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
126
228
|
}
|
|
127
229
|
/**
|
|
128
230
|
* Format log arguments into a single message string
|
|
@@ -204,3 +306,172 @@ function extractSource(stackTrace) {
|
|
|
204
306
|
}
|
|
205
307
|
return undefined;
|
|
206
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Mark the logging system as initialized and flush any pending logs
|
|
311
|
+
*/
|
|
312
|
+
function markLoggingInitialized() {
|
|
313
|
+
isInitialized = true;
|
|
314
|
+
// Flush pending logs sequentially with small delays to avoid recursion guard
|
|
315
|
+
if (pendingLogs.length > 0) {
|
|
316
|
+
originalConsole.log(`[console-logger] Flushing ${pendingLogs.length} pending logs...`);
|
|
317
|
+
const logsToFlush = [...pendingLogs]; // Copy array
|
|
318
|
+
pendingLogs.length = 0; // Clear the original array
|
|
319
|
+
// Flush logs with small delays between them
|
|
320
|
+
let delay = 0;
|
|
321
|
+
for (const { level, source, args } of logsToFlush) {
|
|
322
|
+
setTimeout(() => {
|
|
323
|
+
writeLogToDatabaseWithSource(level, source, args).catch((err) => {
|
|
324
|
+
originalConsole.error('[console-logger] Failed to flush pending log:', err);
|
|
325
|
+
});
|
|
326
|
+
}, delay);
|
|
327
|
+
delay += 10; // 10ms between each log
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Reset logging initialization state (primarily for testing)
|
|
333
|
+
* @internal
|
|
334
|
+
*/
|
|
335
|
+
function resetLoggingState() {
|
|
336
|
+
isInitialized = false;
|
|
337
|
+
pendingLogs.length = 0;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Write a log entry to the ConsoleLogs table with a specific source
|
|
341
|
+
*/
|
|
342
|
+
async function writeLogToDatabaseWithSource(level, source, args) {
|
|
343
|
+
const timestamp = (0, utils_1.getTimestamp)();
|
|
344
|
+
if (timestamp < startupDelayForWritingLogsToDB) {
|
|
345
|
+
await (0, utils_1.sleep)(startupDelayForWritingLogsToDB - timestamp);
|
|
346
|
+
}
|
|
347
|
+
// Guard against infinite recursion (same process)
|
|
348
|
+
if (isWritingLog) {
|
|
349
|
+
recursionAttempts++;
|
|
350
|
+
if (recursionAttempts >= MAX_RECURSION_ATTEMPTS) {
|
|
351
|
+
originalConsole.error(`[console-logger] Infinite loop detected after ${recursionAttempts} attempts, dropping log write`, { level, source, args });
|
|
352
|
+
setTimeout(() => {
|
|
353
|
+
recursionAttempts = 0;
|
|
354
|
+
}, 1000);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
isWritingLog = true;
|
|
360
|
+
try {
|
|
361
|
+
// Format message from arguments
|
|
362
|
+
const message = formatLogMessage(args);
|
|
363
|
+
// Extract context objects from arguments
|
|
364
|
+
const context = extractContext(args);
|
|
365
|
+
// Get stack trace for errors
|
|
366
|
+
const stackTrace = level === 'error' ? getStackTrace() : undefined;
|
|
367
|
+
// Guard against cross-process infinite loops
|
|
368
|
+
const coreMessage = extractCoreMessage(message);
|
|
369
|
+
const signature = `${level}:${source}:${coreMessage.substring(0, 100)}`;
|
|
370
|
+
if (!checkLogSignature(signature)) {
|
|
371
|
+
originalConsole.warn(`[console-logger] Cross-process infinite loop detected for log: ${signature.substring(0, 80)}...`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const logRecord = {
|
|
375
|
+
logId: (0, utils_1.newid)(),
|
|
376
|
+
timestamp,
|
|
377
|
+
level,
|
|
378
|
+
process: currentProcessName,
|
|
379
|
+
processInstanceId: currentProcessInstanceId,
|
|
380
|
+
source,
|
|
381
|
+
message,
|
|
382
|
+
context,
|
|
383
|
+
stackTrace,
|
|
384
|
+
};
|
|
385
|
+
const consoleLogsTable = await (0, console_logs_table_1.ConsoleLogs)();
|
|
386
|
+
await consoleLogsTable.insert(logRecord);
|
|
387
|
+
}
|
|
388
|
+
catch (err) {
|
|
389
|
+
// Silently fail if table not available
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
isWritingLog = false;
|
|
393
|
+
recursionAttempts = 0;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Logger class for creating source-specific loggers
|
|
398
|
+
*
|
|
399
|
+
* Usage:
|
|
400
|
+
* ```typescript
|
|
401
|
+
* const logger = new Logger('MyModule');
|
|
402
|
+
* logger.log('Something happened');
|
|
403
|
+
* logger.error('An error occurred');
|
|
404
|
+
* ```
|
|
405
|
+
*/
|
|
406
|
+
class Logger {
|
|
407
|
+
source;
|
|
408
|
+
constructor(source) {
|
|
409
|
+
// If no source provided, try to extract from caller's location
|
|
410
|
+
if (!source) {
|
|
411
|
+
const stack = new Error().stack;
|
|
412
|
+
source = extractSource(stack) || 'unknown';
|
|
413
|
+
}
|
|
414
|
+
this.source = source;
|
|
415
|
+
// ConsoleLogs().then(() => {
|
|
416
|
+
// markLoggingInitialized();
|
|
417
|
+
// })
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Log a debug message
|
|
421
|
+
*/
|
|
422
|
+
debug(...args) {
|
|
423
|
+
originalConsole.debug(...args);
|
|
424
|
+
this.writeLog('debug', args);
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Log an info message
|
|
428
|
+
*/
|
|
429
|
+
info(...args) {
|
|
430
|
+
originalConsole.info(...args);
|
|
431
|
+
this.writeLog('info', args);
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Log a general message
|
|
435
|
+
*/
|
|
436
|
+
log(...args) {
|
|
437
|
+
originalConsole.log(...args);
|
|
438
|
+
this.writeLog('log', args);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Log a warning message
|
|
442
|
+
*/
|
|
443
|
+
warn(...args) {
|
|
444
|
+
originalConsole.warn(...args);
|
|
445
|
+
this.writeLog('warn', args);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Log an error message
|
|
449
|
+
*/
|
|
450
|
+
error(...args) {
|
|
451
|
+
originalConsole.error(...args);
|
|
452
|
+
this.writeLog('error', args);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Internal method to write log to database
|
|
456
|
+
*/
|
|
457
|
+
writeLog(level, args) {
|
|
458
|
+
if (!isInitialized) {
|
|
459
|
+
// Queue the log until system is initialized
|
|
460
|
+
if (pendingLogs.length < MAX_PENDING_LOGS) {
|
|
461
|
+
pendingLogs.push({ level, source: this.source, args });
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
// Drop logs if queue is full to prevent memory issues
|
|
465
|
+
if (pendingLogs.length === MAX_PENDING_LOGS) {
|
|
466
|
+
originalConsole.warn('[console-logger] Pending log queue is full, dropping new logs until initialization');
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
// System is initialized, write directly
|
|
472
|
+
writeLogToDatabaseWithSource(level, this.source, args).catch((err) => {
|
|
473
|
+
originalConsole.error('[console-logger] Failed to write log:', err);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
exports.Logger = Logger;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const console_logger_1 = require("./console-logger");
|
|
4
|
+
// Mock the ConsoleLogs table
|
|
5
|
+
jest.mock('./console-logs.table', () => {
|
|
6
|
+
const mockTable = {
|
|
7
|
+
insert: jest.fn().mockResolvedValue(undefined),
|
|
8
|
+
dataChanged: {
|
|
9
|
+
subscribe: jest.fn(),
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
ConsoleLogs: jest.fn().mockResolvedValue(mockTable),
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
// Mock utils
|
|
17
|
+
jest.mock('../utils', () => ({
|
|
18
|
+
newid: () => 'test-id-123',
|
|
19
|
+
getTimestamp: () => '2024-01-01T00:00:00.000Z',
|
|
20
|
+
}));
|
|
21
|
+
describe('console-logger', () => {
|
|
22
|
+
let originalConsole;
|
|
23
|
+
describe('extractCoreMessage', () => {
|
|
24
|
+
it('should extract core message from messages with [prefix] tags', () => {
|
|
25
|
+
expect((0, console_logger_1.extractCoreMessage)('[main] Error message')).toBe('Error message');
|
|
26
|
+
expect((0, console_logger_1.extractCoreMessage)('[renderer] [main] Error message')).toBe('Error message');
|
|
27
|
+
expect((0, console_logger_1.extractCoreMessage)('[process-1] [process-2] [process-3] Error message')).toBe('Error message');
|
|
28
|
+
});
|
|
29
|
+
it('should remove Error: prefix', () => {
|
|
30
|
+
expect((0, console_logger_1.extractCoreMessage)('Error: Something went wrong')).toBe('Something went wrong');
|
|
31
|
+
expect((0, console_logger_1.extractCoreMessage)('[main] Error: Something went wrong')).toBe('Something went wrong');
|
|
32
|
+
});
|
|
33
|
+
it('should remove timestamp patterns', () => {
|
|
34
|
+
expect((0, console_logger_1.extractCoreMessage)('2024-01-01T12:00:00.000Z Error message')).toBe('Error message');
|
|
35
|
+
expect((0, console_logger_1.extractCoreMessage)('2024-01-01 12:00:00 Error message')).toBe('Error message');
|
|
36
|
+
});
|
|
37
|
+
it('should handle combinations of prefixes', () => {
|
|
38
|
+
expect((0, console_logger_1.extractCoreMessage)('[renderer] 2024-01-01T12:00:00.000Z Error: Failed to write log')).toBe('Failed to write log');
|
|
39
|
+
expect((0, console_logger_1.extractCoreMessage)('[main] [renderer] Error: Failed to write log')).toBe('Failed to write log');
|
|
40
|
+
});
|
|
41
|
+
it('should return the same message if no prefixes found', () => {
|
|
42
|
+
expect((0, console_logger_1.extractCoreMessage)('Plain error message')).toBe('Plain error message');
|
|
43
|
+
});
|
|
44
|
+
it('should handle empty or whitespace messages', () => {
|
|
45
|
+
expect((0, console_logger_1.extractCoreMessage)('')).toBe('');
|
|
46
|
+
expect((0, console_logger_1.extractCoreMessage)(' ')).toBe('');
|
|
47
|
+
expect((0, console_logger_1.extractCoreMessage)('[main]')).toBe('');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
beforeAll(() => {
|
|
51
|
+
// Save original console methods
|
|
52
|
+
originalConsole = {
|
|
53
|
+
debug: console.debug,
|
|
54
|
+
info: console.info,
|
|
55
|
+
log: console.log,
|
|
56
|
+
warn: console.warn,
|
|
57
|
+
error: console.error,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
// Clear all mocks
|
|
62
|
+
jest.clearAllMocks();
|
|
63
|
+
// Restore console before each test
|
|
64
|
+
(0, console_logger_1.restoreConsole)();
|
|
65
|
+
});
|
|
66
|
+
afterAll(() => {
|
|
67
|
+
// Restore original console methods
|
|
68
|
+
console.debug = originalConsole.debug;
|
|
69
|
+
console.info = originalConsole.info;
|
|
70
|
+
console.log = originalConsole.log;
|
|
71
|
+
console.warn = originalConsole.warn;
|
|
72
|
+
console.error = originalConsole.error;
|
|
73
|
+
});
|
|
74
|
+
describe('Single-process infinite loop detection', () => {
|
|
75
|
+
it('should detect infinite loop when logging from within log handler', async () => {
|
|
76
|
+
const { ConsoleLogs } = require('./console-logs.table');
|
|
77
|
+
const mockTable = await ConsoleLogs();
|
|
78
|
+
// Setup mock to throw error that triggers another log
|
|
79
|
+
let callCount = 0;
|
|
80
|
+
mockTable.insert.mockImplementation(() => {
|
|
81
|
+
callCount++;
|
|
82
|
+
if (callCount < 10) {
|
|
83
|
+
// This would trigger another log attempt
|
|
84
|
+
throw new Error('Database error');
|
|
85
|
+
}
|
|
86
|
+
return Promise.resolve();
|
|
87
|
+
});
|
|
88
|
+
await (0, console_logger_1.setupConsoleProxy)('test-process');
|
|
89
|
+
// This should trigger the loop
|
|
90
|
+
console.error('Initial error');
|
|
91
|
+
// Wait for async operations
|
|
92
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
93
|
+
// Should have called insert but stopped early due to recursion guard
|
|
94
|
+
expect(mockTable.insert).toHaveBeenCalled();
|
|
95
|
+
// The recursion guard should stop it after just 1 attempt
|
|
96
|
+
// (because isWritingLog prevents re-entry)
|
|
97
|
+
expect(callCount).toBeLessThanOrEqual(2); // Setup log + 1 error attempt
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe('Cross-process infinite loop detection', () => {
|
|
101
|
+
it('should detect when same log is written too many times in window', async () => {
|
|
102
|
+
const { ConsoleLogs } = require('./console-logs.table');
|
|
103
|
+
const mockTable = await ConsoleLogs();
|
|
104
|
+
await (0, console_logger_1.setupConsoleProxy)('test-process');
|
|
105
|
+
// Log the same message many times quickly (simulating cross-process loop)
|
|
106
|
+
for (let i = 0; i < 15; i++) {
|
|
107
|
+
console.error('Same error message');
|
|
108
|
+
// Small delay to ensure they're processed
|
|
109
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
110
|
+
}
|
|
111
|
+
// Wait for async operations
|
|
112
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
113
|
+
// Should have stopped writing after threshold (10)
|
|
114
|
+
// Plus 1 for the setup log
|
|
115
|
+
expect(mockTable.insert.mock.calls.length).toBeLessThan(15);
|
|
116
|
+
expect(mockTable.insert.mock.calls.length).toBeGreaterThan(0);
|
|
117
|
+
});
|
|
118
|
+
it('should NOT detect loop for different messages', async () => {
|
|
119
|
+
const { ConsoleLogs } = require('./console-logs.table');
|
|
120
|
+
const mockTable = await ConsoleLogs();
|
|
121
|
+
await (0, console_logger_1.setupConsoleProxy)('test-process');
|
|
122
|
+
// Clear setup log
|
|
123
|
+
mockTable.insert.mockClear();
|
|
124
|
+
// Log different messages
|
|
125
|
+
for (let i = 0; i < 15; i++) {
|
|
126
|
+
console.error(`Different error message ${i}`);
|
|
127
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
128
|
+
}
|
|
129
|
+
// Wait for async operations
|
|
130
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
131
|
+
// Should have written most/all messages (allow for timing variance)
|
|
132
|
+
expect(mockTable.insert.mock.calls.length).toBeGreaterThanOrEqual(14);
|
|
133
|
+
expect(mockTable.insert.mock.calls.length).toBeLessThanOrEqual(15);
|
|
134
|
+
});
|
|
135
|
+
it('should detect loop even with modified messages (prefix added)', async () => {
|
|
136
|
+
const { ConsoleLogs } = require('./console-logs.table');
|
|
137
|
+
const mockTable = await ConsoleLogs();
|
|
138
|
+
await (0, console_logger_1.setupConsoleProxy)('test-process');
|
|
139
|
+
// Simulate cross-process loop where each process adds a prefix
|
|
140
|
+
// This represents Process A logging "Error X"
|
|
141
|
+
// Process B seeing it and logging "[main] Error X"
|
|
142
|
+
// Process A seeing that and logging "[renderer] [main] Error X"
|
|
143
|
+
const baseMessage = 'Failed to write log to database';
|
|
144
|
+
const messages = [
|
|
145
|
+
baseMessage,
|
|
146
|
+
`[main] ${baseMessage}`,
|
|
147
|
+
`[renderer] [main] ${baseMessage}`,
|
|
148
|
+
`[main] [renderer] [main] ${baseMessage}`,
|
|
149
|
+
`[renderer] [main] [renderer] [main] ${baseMessage}`,
|
|
150
|
+
];
|
|
151
|
+
// Log these messages in sequence, simulating the loop
|
|
152
|
+
// Need enough to trigger the threshold (MAX_IDENTICAL_LOGS_IN_WINDOW = 10)
|
|
153
|
+
for (let cycle = 0; cycle < 3; cycle++) {
|
|
154
|
+
for (const msg of messages) {
|
|
155
|
+
console.error(msg);
|
|
156
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Wait for async operations
|
|
160
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
161
|
+
// Should detect this as a loop even though messages have different prefixes
|
|
162
|
+
// because extractCoreMessage normalizes them all to the same core message
|
|
163
|
+
// We logged 15 messages (5 variants x 3 cycles), but they all have the same core
|
|
164
|
+
// So after 10, it should start blocking
|
|
165
|
+
const insertCount = mockTable.insert.mock.calls.length;
|
|
166
|
+
// Should be: 1 (setup) + 10 (threshold) = 11, might be a bit more due to timing
|
|
167
|
+
expect(insertCount).toBeGreaterThan(1); // At least setup + some logs
|
|
168
|
+
expect(insertCount).toBeLessThan(17); // Definitely less than all 15 + setup
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
describe('Loop detection recovery', () => {
|
|
172
|
+
it('should allow logging again after window expires', async () => {
|
|
173
|
+
const { ConsoleLogs } = require('./console-logs.table');
|
|
174
|
+
const mockTable = await ConsoleLogs();
|
|
175
|
+
await (0, console_logger_1.setupConsoleProxy)('test-process');
|
|
176
|
+
// Trigger loop detection
|
|
177
|
+
for (let i = 0; i < 12; i++) {
|
|
178
|
+
console.error('Same error');
|
|
179
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
180
|
+
}
|
|
181
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
182
|
+
const firstBatchCount = mockTable.insert.mock.calls.length;
|
|
183
|
+
// Wait for window to expire (5 seconds in real code, but we can't easily test that)
|
|
184
|
+
// Just verify the blocking is in place
|
|
185
|
+
console.error('Same error');
|
|
186
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
187
|
+
// Should still be blocked
|
|
188
|
+
expect(mockTable.insert.mock.calls.length).toBe(firstBatchCount);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
describe('Normal logging behavior', () => {
|
|
192
|
+
it('should write logs to database for normal usage', async () => {
|
|
193
|
+
const { ConsoleLogs } = require('./console-logs.table');
|
|
194
|
+
await (0, console_logger_1.setupConsoleProxy)('test-process');
|
|
195
|
+
// Get fresh mock reference after setup
|
|
196
|
+
const mockTable = await ConsoleLogs();
|
|
197
|
+
mockTable.insert.mockClear();
|
|
198
|
+
console.log('Test message');
|
|
199
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
200
|
+
console.error('Test error');
|
|
201
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
202
|
+
console.warn('Test warning');
|
|
203
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
204
|
+
// Verify at least the logs were attempted
|
|
205
|
+
expect(mockTable.insert).toHaveBeenCalled();
|
|
206
|
+
expect(mockTable.insert.mock.calls.length).toBeGreaterThanOrEqual(1);
|
|
207
|
+
expect(mockTable.insert.mock.calls.length).toBeLessThanOrEqual(3);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
describe('Logger class', () => {
|
|
211
|
+
beforeEach(() => {
|
|
212
|
+
// Reset state before each Logger test
|
|
213
|
+
(0, console_logger_1.resetLoggingState)();
|
|
214
|
+
});
|
|
215
|
+
it('should create logger with specified source', async () => {
|
|
216
|
+
const { ConsoleLogs } = require('./console-logs.table');
|
|
217
|
+
const mockTable = await ConsoleLogs();
|
|
218
|
+
(0, console_logger_1.markLoggingInitialized)();
|
|
219
|
+
const logger = new console_logger_1.Logger('MyModule');
|
|
220
|
+
mockTable.insert.mockClear();
|
|
221
|
+
logger.log('Test message');
|
|
222
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
223
|
+
expect(mockTable.insert).toHaveBeenCalled();
|
|
224
|
+
const call = mockTable.insert.mock.calls[0];
|
|
225
|
+
expect(call[0].source).toBe('MyModule');
|
|
226
|
+
expect(call[0].message).toBe('Test message');
|
|
227
|
+
});
|
|
228
|
+
it('should support all log levels', async () => {
|
|
229
|
+
const { ConsoleLogs } = require('./console-logs.table');
|
|
230
|
+
const mockTable = await ConsoleLogs();
|
|
231
|
+
(0, console_logger_1.markLoggingInitialized)();
|
|
232
|
+
const logger = new console_logger_1.Logger('TestModule');
|
|
233
|
+
mockTable.insert.mockClear();
|
|
234
|
+
logger.debug('Debug message');
|
|
235
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
236
|
+
logger.info('Info message');
|
|
237
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
238
|
+
logger.log('Log message');
|
|
239
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
240
|
+
logger.warn('Warn message');
|
|
241
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
242
|
+
logger.error('Error message');
|
|
243
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
244
|
+
// Should have most/all logs
|
|
245
|
+
expect(mockTable.insert.mock.calls.length).toBeGreaterThanOrEqual(4);
|
|
246
|
+
// Check that different levels were used
|
|
247
|
+
const levels = mockTable.insert.mock.calls.map((call) => call[0].level);
|
|
248
|
+
const uniqueLevels = new Set(levels);
|
|
249
|
+
expect(uniqueLevels.size).toBeGreaterThanOrEqual(4);
|
|
250
|
+
});
|
|
251
|
+
it('should queue logs before initialization', async () => {
|
|
252
|
+
const { ConsoleLogs } = require('./console-logs.table');
|
|
253
|
+
const mockTable = await ConsoleLogs();
|
|
254
|
+
// Ensure we're not initialized
|
|
255
|
+
// (resetLoggingState was called in beforeEach)
|
|
256
|
+
const logger = new console_logger_1.Logger('EarlyModule');
|
|
257
|
+
// Log before initialization
|
|
258
|
+
logger.log('Early message 1');
|
|
259
|
+
logger.warn('Early message 2');
|
|
260
|
+
logger.error('Early message 3');
|
|
261
|
+
// Small delay to ensure logs are queued
|
|
262
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
263
|
+
mockTable.insert.mockClear();
|
|
264
|
+
// Should not have written yet
|
|
265
|
+
expect(mockTable.insert).not.toHaveBeenCalled();
|
|
266
|
+
// Now initialize
|
|
267
|
+
(0, console_logger_1.markLoggingInitialized)();
|
|
268
|
+
// Wait for flush
|
|
269
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
270
|
+
// Should have flushed pending logs
|
|
271
|
+
expect(mockTable.insert.mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
272
|
+
});
|
|
273
|
+
it('should write logs directly after initialization', async () => {
|
|
274
|
+
const { ConsoleLogs } = require('./console-logs.table');
|
|
275
|
+
const mockTable = await ConsoleLogs();
|
|
276
|
+
(0, console_logger_1.markLoggingInitialized)();
|
|
277
|
+
const logger = new console_logger_1.Logger('LateModule');
|
|
278
|
+
mockTable.insert.mockClear();
|
|
279
|
+
logger.log('Direct message');
|
|
280
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
281
|
+
expect(mockTable.insert).toHaveBeenCalledTimes(1);
|
|
282
|
+
expect(mockTable.insert.mock.calls[0][0].message).toBe('Direct message');
|
|
283
|
+
});
|
|
284
|
+
it('should use reliable source from constructor', async () => {
|
|
285
|
+
const { ConsoleLogs } = require('./console-logs.table');
|
|
286
|
+
const mockTable = await ConsoleLogs();
|
|
287
|
+
(0, console_logger_1.markLoggingInitialized)();
|
|
288
|
+
const logger1 = new console_logger_1.Logger('SourceA');
|
|
289
|
+
const logger2 = new console_logger_1.Logger('SourceB');
|
|
290
|
+
mockTable.insert.mockClear();
|
|
291
|
+
logger1.log('From A');
|
|
292
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
293
|
+
logger2.log('From B');
|
|
294
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
295
|
+
expect(mockTable.insert).toHaveBeenCalledTimes(2);
|
|
296
|
+
expect(mockTable.insert.mock.calls[0][0].source).toBe('SourceA');
|
|
297
|
+
expect(mockTable.insert.mock.calls[1][0].source).toBe('SourceB');
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -13,9 +13,9 @@ export declare const consoleLogSchema: z.ZodObject<{
|
|
|
13
13
|
stackTrace: z.ZodOptional<z.ZodString>;
|
|
14
14
|
}, "strip", z.ZodTypeAny, {
|
|
15
15
|
message: string;
|
|
16
|
+
level: "error" | "debug" | "info" | "log" | "warn";
|
|
16
17
|
timestamp: number;
|
|
17
18
|
logId: string;
|
|
18
|
-
level: "error" | "debug" | "info" | "log" | "warn";
|
|
19
19
|
process: string;
|
|
20
20
|
processInstanceId: string;
|
|
21
21
|
source?: string | undefined;
|
|
@@ -23,8 +23,8 @@ export declare const consoleLogSchema: z.ZodObject<{
|
|
|
23
23
|
stackTrace?: string | undefined;
|
|
24
24
|
}, {
|
|
25
25
|
message: string;
|
|
26
|
-
logId: string;
|
|
27
26
|
level: "error" | "debug" | "info" | "log" | "warn";
|
|
27
|
+
logId: string;
|
|
28
28
|
process: string;
|
|
29
29
|
processInstanceId: string;
|
|
30
30
|
source?: string | undefined;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example usage of the Logger class
|
|
3
|
+
*
|
|
4
|
+
* This file demonstrates how to use the Logger class for reliable, source-specific logging
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Example function that uses the logger
|
|
8
|
+
*/
|
|
9
|
+
export declare function processData(data: any): void;
|
|
10
|
+
/**
|
|
11
|
+
* Example initialization flow
|
|
12
|
+
*/
|
|
13
|
+
export declare function initializeApp(): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Benefits of using Logger class:
|
|
16
|
+
*
|
|
17
|
+
* 1. **Reliable Source Tracking**: Source is set once at construction time,
|
|
18
|
+
* not extracted from stack traces which can be unreliable
|
|
19
|
+
*
|
|
20
|
+
* 2. **Initialization Safety**: Logs before system is ready are queued and
|
|
21
|
+
* flushed once markLoggingInitialized() is called
|
|
22
|
+
*
|
|
23
|
+
* 3. **Infinite Loop Protection**: Inherits all the cross-process and
|
|
24
|
+
* single-process infinite loop detection
|
|
25
|
+
*
|
|
26
|
+
* 4. **Type Safety**: Full TypeScript support with proper typing
|
|
27
|
+
*
|
|
28
|
+
* 5. **Console Passthrough**: All logs still appear in the console immediately,
|
|
29
|
+
* database writes happen asynchronously
|
|
30
|
+
*
|
|
31
|
+
* Usage pattern:
|
|
32
|
+
* ```typescript
|
|
33
|
+
* // At the top of each file:
|
|
34
|
+
* const logger = new Logger('FeatureName');
|
|
35
|
+
*
|
|
36
|
+
* // Throughout your code:
|
|
37
|
+
* logger.log('Something happened');
|
|
38
|
+
* logger.error('Something went wrong', error);
|
|
39
|
+
* logger.debug('Detailed debugging info', { context });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|