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.
144 lines
3.5 KiB
JavaScript
144 lines
3.5 KiB
JavaScript
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
const logStreamAuth = require('../middleware/logStreamAuth');
|
|
const {
|
|
logEmitter,
|
|
logBuffer,
|
|
clientLogBuffer,
|
|
ingestClientLogs
|
|
} = require('../utils/logCapture');
|
|
|
|
// Public status check (no auth, no 403 block, returns standard config state)
|
|
router.get('/status', (req, res) => {
|
|
res.json({ enabled: process.env.ENABLE_LOG_STREAM === 'true' });
|
|
});
|
|
|
|
// Global toggle check
|
|
router.use((req, res, next) => {
|
|
if (process.env.ENABLE_LOG_STREAM !== 'true') {
|
|
return res.status(403).json({ error: 'Log streaming feature is disabled' });
|
|
}
|
|
next();
|
|
});
|
|
|
|
// Enforce subnet and authentication validations on all debug routes
|
|
router.use(logStreamAuth);
|
|
|
|
/**
|
|
* GET /api/debug/server-logs
|
|
* Exposes a real-time SSE stream of intercepted server stdout/stderr logs.
|
|
*/
|
|
router.get('/server-logs', (req, res) => {
|
|
res.writeHead(200, {
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache',
|
|
'Connection': 'keep-alive',
|
|
'X-Accel-Buffering': 'no'
|
|
});
|
|
|
|
// Send historical server logs buffer first
|
|
for (const line of logBuffer) {
|
|
res.write(`data: ${line}\n\n`);
|
|
}
|
|
|
|
// Gracefully close for integration testing
|
|
if (req.query.testClose === 'true') {
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
const sendLog = (line) => {
|
|
try {
|
|
res.write(`data: ${line}\n\n`);
|
|
} catch (err) {
|
|
console.error('[debugRoutes] Error sending server log line:', err.message);
|
|
}
|
|
};
|
|
|
|
logEmitter.on('server-log', sendLog);
|
|
|
|
// 25s heartbeat comment to prevent proxy timeouts
|
|
const heartbeat = setInterval(() => {
|
|
try {
|
|
res.write(': heartbeat\n\n');
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}, 25000);
|
|
|
|
req.on('close', () => {
|
|
clearInterval(heartbeat);
|
|
logEmitter.off('server-log', sendLog);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* GET /api/debug/client-logs
|
|
* Exposes a real-time SSE stream of ingested client-side console logs.
|
|
*/
|
|
router.get('/client-logs', (req, res) => {
|
|
res.writeHead(200, {
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache',
|
|
'Connection': 'keep-alive',
|
|
'X-Accel-Buffering': 'no'
|
|
});
|
|
|
|
// Send historical client logs buffer first
|
|
for (const line of clientLogBuffer) {
|
|
res.write(`data: ${line}\n\n`);
|
|
}
|
|
|
|
// Gracefully close for integration testing
|
|
if (req.query.testClose === 'true') {
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
const sendClientLog = (line) => {
|
|
try {
|
|
res.write(`data: ${line}\n\n`);
|
|
} catch (err) {
|
|
console.error('[debugRoutes] Error sending client log line:', err.message);
|
|
}
|
|
};
|
|
|
|
logEmitter.on('client-log', sendClientLog);
|
|
|
|
// 25s heartbeat comment to prevent proxy timeouts
|
|
const heartbeat = setInterval(() => {
|
|
try {
|
|
res.write(': heartbeat\n\n');
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}, 25000);
|
|
|
|
req.on('close', () => {
|
|
clearInterval(heartbeat);
|
|
logEmitter.off('client-log', sendClientLog);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* POST /api/debug/client-logs
|
|
* Receives batches of frontend console logs to store in buffer and emit.
|
|
*/
|
|
router.post('/client-logs', (req, res) => {
|
|
const logs = req.body;
|
|
if (!Array.isArray(logs)) {
|
|
return res.status(400).json({ error: 'Body must be a JSON array of log entries' });
|
|
}
|
|
|
|
try {
|
|
ingestClientLogs(logs);
|
|
return res.status(200).json({ success: true, count: logs.length });
|
|
} catch (err) {
|
|
console.error('[debugRoutes] Ingestion failed:', err.message);
|
|
return res.status(500).json({ error: 'Internal server error during ingestion' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|