3c6791658c
- Created server/utils/logCapture.js to intercept and buffer server output, stripping ANSI escape codes. - Created server/middleware/logStreamAuth.js enforcing subnet IP filtering (LOG_ALLOW_SUBNETS), Emby session cookie, Basic Auth fallback, and X-Webhook-Secret header bypass. - Created server/routes/debug.js with SSE streams /api/debug/server-logs, /api/debug/client-logs and batched POST /api/debug/client-logs. Exposes public configuration status at /api/debug/status. - Integrated log capture and mounted debug routes in server/app.js and server/index.js. - Implemented client/src/utils/clientLogCapture.js in the frontend SPA to hook console log/warn/error and flush batched console events. - Documented all endpoints in OpenAPI server/openapi.yaml, ARCHITECTURE.md, and README.md. - Wrote route integration tests and frontend console capture tests, with full validation in swagger-coverage.
138 lines
3.7 KiB
JavaScript
138 lines
3.7 KiB
JavaScript
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
|
|
|
const logQueue = [];
|
|
const MAX_QUEUE_SIZE = 20;
|
|
const FLUSH_INTERVAL_MS = 2000;
|
|
|
|
// Original console functions
|
|
const originalLog = console.log;
|
|
const originalWarn = console.warn;
|
|
const originalError = console.error;
|
|
|
|
let isSending = false;
|
|
let isInitialized = false;
|
|
let flushInterval = null;
|
|
|
|
function formatArgs(args) {
|
|
return args.map(arg => {
|
|
if (arg === null) return 'null';
|
|
if (arg === undefined) return 'undefined';
|
|
if (arg instanceof Error) {
|
|
return `${arg.name}: ${arg.message}\n${arg.stack || ''}`;
|
|
}
|
|
if (typeof arg === 'object') {
|
|
try {
|
|
return JSON.stringify(arg);
|
|
} catch {
|
|
return String(arg);
|
|
}
|
|
}
|
|
return String(arg);
|
|
}).join(' ');
|
|
}
|
|
|
|
function enqueue(level, args) {
|
|
const formattedMsg = formatArgs(args);
|
|
|
|
// Still write to the developer console!
|
|
if (level === 'info') originalLog.apply(console, args);
|
|
else if (level === 'warn') originalWarn.apply(console, args);
|
|
else if (level === 'error') originalError.apply(console, args);
|
|
|
|
// Guard against infinite loop during logs dispatching
|
|
if (isSending) return;
|
|
|
|
logQueue.push({
|
|
timestamp: new Date().toISOString(),
|
|
level,
|
|
message: formattedMsg
|
|
});
|
|
|
|
// Flush immediately if queue is full
|
|
if (logQueue.length >= MAX_QUEUE_SIZE) {
|
|
flushQueue();
|
|
}
|
|
}
|
|
|
|
async function flushQueue() {
|
|
if (logQueue.length === 0 || isSending) return;
|
|
|
|
isSending = true;
|
|
const batch = [...logQueue];
|
|
logQueue.length = 0;
|
|
|
|
try {
|
|
const response = await fetch('/api/debug/client-logs', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(batch),
|
|
// keepalive allows request to survive page unload
|
|
keepalive: true
|
|
});
|
|
|
|
if (!response.ok) {
|
|
originalError.call(console, '[clientLogCapture] Ingestion server returned error status:', response.status);
|
|
}
|
|
} catch (err) {
|
|
originalError.call(console, '[clientLogCapture] Ingestion post request failed:', err.message);
|
|
} finally {
|
|
isSending = false;
|
|
}
|
|
}
|
|
|
|
// Perform a fast/unblocked payload flush using sendBeacon on page unload
|
|
function flushOnUnload() {
|
|
if (logQueue.length === 0) return;
|
|
|
|
const batch = [...logQueue];
|
|
logQueue.length = 0;
|
|
|
|
try {
|
|
const blob = new Blob([JSON.stringify(batch)], { type: 'application/json' });
|
|
navigator.sendBeacon('/api/debug/client-logs', blob);
|
|
} catch (err) {
|
|
// sendBeacon failure, fallback to synchronous fetch with keepalive if available
|
|
try {
|
|
fetch('/api/debug/client-logs', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(batch),
|
|
keepalive: true
|
|
});
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function initClientLogCapture() {
|
|
if (isInitialized) return;
|
|
|
|
try {
|
|
// 1. Check if the server toggle for logging is active
|
|
const response = await fetch('/api/debug/status');
|
|
if (!response.ok) return;
|
|
|
|
const data = await response.json();
|
|
if (data && data.enabled === true) {
|
|
// 2. Override global console methods
|
|
console.log = (...args) => enqueue('info', args);
|
|
console.warn = (...args) => enqueue('warn', args);
|
|
console.error = (...args) => enqueue('error', args);
|
|
|
|
// 3. Set interval for batch updates
|
|
flushInterval = setInterval(flushQueue, FLUSH_INTERVAL_MS);
|
|
|
|
// 4. Setup beforeunload listener for clean flushing
|
|
window.addEventListener('beforeunload', flushOnUnload);
|
|
|
|
isInitialized = true;
|
|
console.log('[clientLogCapture] Browser console logging interceptor initialized successfully.');
|
|
}
|
|
} catch (err) {
|
|
originalError.call(console, '[clientLogCapture] Check failed to start interceptor:', err.message);
|
|
}
|
|
}
|