forked from snxraven/p2ns
Upgrade logs
This commit is contained in:
+2
-1
@@ -9,4 +9,5 @@ plugin-sites/**/app.log
|
|||||||
plugin-sites/**/db
|
plugin-sites/**/db
|
||||||
plugin-sites/**/spec
|
plugin-sites/**/spec
|
||||||
plugin-sites/**/drives
|
plugin-sites/**/drives
|
||||||
plugin-sites/replication.example
|
plugin-sites/replication.example
|
||||||
|
logs/
|
||||||
+7
-22
@@ -1,34 +1,19 @@
|
|||||||
// Main admin entry point - imports all modules and sets up console overrides
|
// Main admin entry point
|
||||||
const { adminWss, broadcast, closeAllWebSockets } = require('./admin/admin-backend/websocket');
|
const { adminWss, broadcast, closeAllWebSockets } = require('./admin/admin-backend/websocket');
|
||||||
const { loadHolesailServers, startHolesailServer, saveHolesailServers } = require('./admin/admin-backend/holesail-servers');
|
const { loadHolesailServers, startHolesailServer, saveHolesailServers } = require('./admin/admin-backend/holesail-servers');
|
||||||
const { loadHolesailClients, startForkedHolesailClient, saveHolesailClients } = require('./admin/admin-backend/holesail-clients');
|
const { loadHolesailClients, startForkedHolesailClient, saveHolesailClients } = require('./admin/admin-backend/holesail-clients');
|
||||||
const { loadSelectorCache, saveSelectorCache, loadBlockedPeers, saveBlockedPeers, loadPeerMetrics, savePeerMetrics, loadPeerHistory, savePeerHistory } = require('./admin/admin-backend/cache');
|
const { loadSelectorCache, saveSelectorCache, loadBlockedPeers, saveBlockedPeers, loadPeerMetrics, savePeerMetrics, loadPeerHistory, savePeerHistory } = require('./admin/admin-backend/cache');
|
||||||
const { settingsMetadata, envWhitelist } = require('./admin/admin-backend/settings');
|
const { settingsMetadata, envWhitelist } = require('./admin/admin-backend/settings');
|
||||||
const { handleAdminRequest } = require('./admin/admin-backend/routes');
|
const { handleAdminRequest } = require('./admin/admin-backend/routes');
|
||||||
|
const { onLogLine } = require('./infrastructure/logger');
|
||||||
|
const { notifyLogSubscribers } = require('./admin/admin-backend/log-websocket');
|
||||||
|
|
||||||
// Save original console methods for restoration
|
onLogLine((channel, level, message) => {
|
||||||
const originalConsoleLog = console.log;
|
notifyLogSubscribers(channel, level, message);
|
||||||
const originalConsoleError = console.error;
|
});
|
||||||
const originalConsoleWarn = console.warn;
|
|
||||||
const originalConsoleDebug = console.debug;
|
|
||||||
|
|
||||||
// Override console methods to broadcast to WebSocket clients
|
|
||||||
console.log = (...args) => {
|
|
||||||
originalConsoleLog(...args);
|
|
||||||
broadcast({ type: 'log', level: 'info', message: args.join(' ') });
|
|
||||||
};
|
|
||||||
|
|
||||||
console.error = (...args) => {
|
|
||||||
originalConsoleError(...args);
|
|
||||||
broadcast({ type: 'log', level: 'error', message: args.join(' ') });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to restore original console methods
|
|
||||||
function restoreConsoleMethods() {
|
function restoreConsoleMethods() {
|
||||||
console.log = originalConsoleLog;
|
/* no-op: console is no longer overridden for admin log streaming */
|
||||||
console.error = originalConsoleError;
|
|
||||||
console.warn = originalConsoleWarn;
|
|
||||||
console.debug = originalConsoleDebug;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export all public APIs for backward compatibility
|
// Export all public APIs for backward compatibility
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Per-channel log tail subscriptions over admin WebSocket.
|
||||||
|
*/
|
||||||
|
const { getLogTail, listLogChannels } = require('../../infrastructure/log-files');
|
||||||
|
|
||||||
|
/** @type {Map<import('ws'), string>} */
|
||||||
|
const logSubscribers = new Map();
|
||||||
|
|
||||||
|
function handleLogWsMessage(ws, data) {
|
||||||
|
if (data.type === 'subscribe-log') {
|
||||||
|
const channel = data.channel || 'core';
|
||||||
|
if (!listLogChannels().some((c) => c.id === channel)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
logSubscribers.set(ws, channel);
|
||||||
|
if (ws.readyState === 1) {
|
||||||
|
const lines = getLogTail(channel, data.lines || 500);
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'log-snapshot',
|
||||||
|
channel,
|
||||||
|
lines
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (data.type === 'unsubscribe-log') {
|
||||||
|
logSubscribers.delete(ws);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLogWsClose(ws) {
|
||||||
|
logSubscribers.delete(ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyLogSubscribers(channelId, level, line) {
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
type: 'file-log',
|
||||||
|
channel: channelId,
|
||||||
|
level,
|
||||||
|
message: line
|
||||||
|
});
|
||||||
|
for (const [ws, subscribed] of logSubscribers.entries()) {
|
||||||
|
if (subscribed !== channelId) continue;
|
||||||
|
if (ws.readyState === 1) {
|
||||||
|
ws.send(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLogWebSocketState() {
|
||||||
|
logSubscribers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
handleLogWsMessage,
|
||||||
|
onLogWsClose,
|
||||||
|
notifyLogSubscribers,
|
||||||
|
closeLogWebSocketState
|
||||||
|
};
|
||||||
@@ -15,6 +15,7 @@ const { handleBackupsRoutes } = require('./backups');
|
|||||||
const { handleDiagnosticsRoutes } = require('./diagnostics');
|
const { handleDiagnosticsRoutes } = require('./diagnostics');
|
||||||
const { handleConsensusRoutes } = require('./consensus');
|
const { handleConsensusRoutes } = require('./consensus');
|
||||||
const { handlePluginsRoutes } = require('./plugins');
|
const { handlePluginsRoutes } = require('./plugins');
|
||||||
|
const { handleLogsRoutes } = require('./logs');
|
||||||
|
|
||||||
async function handleAdminRequest(req, res) {
|
async function handleAdminRequest(req, res) {
|
||||||
const url = new URL(req.url, `https://${req.headers.host}`);
|
const url = new URL(req.url, `https://${req.headers.host}`);
|
||||||
@@ -52,7 +53,8 @@ async function handleAdminRequest(req, res) {
|
|||||||
if (await handleDiagnosticsRoutes(req, res)) return;
|
if (await handleDiagnosticsRoutes(req, res)) return;
|
||||||
if (await handleConsensusRoutes(req, res)) return;
|
if (await handleConsensusRoutes(req, res)) return;
|
||||||
if (await handlePluginsRoutes(req, res)) return;
|
if (await handlePluginsRoutes(req, res)) return;
|
||||||
|
if (await handleLogsRoutes(req, res)) return;
|
||||||
|
|
||||||
// No route matched
|
// No route matched
|
||||||
res.writeHead(404);
|
res.writeHead(404);
|
||||||
res.end('Not Found');
|
res.end('Not Found');
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
const { listLogChannels, getLogTail } = require('../../../infrastructure/log-files');
|
||||||
|
|
||||||
|
async function handleLogsRoutes(req, res) {
|
||||||
|
const urlPath = req.urlPath;
|
||||||
|
const method = req.method;
|
||||||
|
|
||||||
|
if (method === 'GET' && urlPath === '/api/logs') {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ channels: listLogChannels() }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = urlPath.match(/^\/api\/logs\/([a-z]+)$/);
|
||||||
|
if (method === 'GET' && match) {
|
||||||
|
const channelId = match[1];
|
||||||
|
const channels = listLogChannels();
|
||||||
|
if (!channels.some((c) => c.id === channelId)) {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Unknown log channel' }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const url = new URL(req.url, `https://${req.headers.host}`);
|
||||||
|
const lines = Math.min(parseInt(url.searchParams.get('lines') || '500', 10), 5000);
|
||||||
|
const tail = getLogTail(channelId, lines);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ channel: channelId, lines: tail }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { handleLogsRoutes };
|
||||||
@@ -11,6 +11,11 @@ const {
|
|||||||
notifyStatsSubscribers,
|
notifyStatsSubscribers,
|
||||||
closeStatsWebSocketState
|
closeStatsWebSocketState
|
||||||
} = require('./stats-websocket');
|
} = require('./stats-websocket');
|
||||||
|
const {
|
||||||
|
handleLogWsMessage,
|
||||||
|
onLogWsClose,
|
||||||
|
closeLogWebSocketState
|
||||||
|
} = require('./log-websocket');
|
||||||
|
|
||||||
const adminWss = new WebSocket.Server({ noServer: true });
|
const adminWss = new WebSocket.Server({ noServer: true });
|
||||||
const adminClients = new Set();
|
const adminClients = new Set();
|
||||||
@@ -119,6 +124,8 @@ adminWss.on('connection', (ws) => {
|
|||||||
});
|
});
|
||||||
} else if (handleStatsWsMessage(ws, data)) {
|
} else if (handleStatsWsMessage(ws, data)) {
|
||||||
// subscribe-stats / unsubscribe-stats / request-stats-snapshot
|
// subscribe-stats / unsubscribe-stats / request-stats-snapshot
|
||||||
|
} else if (handleLogWsMessage(ws, data)) {
|
||||||
|
// subscribe-log / unsubscribe-log
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logError('Admin', `Error handling WebSocket message: ${err.message}`);
|
logError('Admin', `Error handling WebSocket message: ${err.message}`);
|
||||||
@@ -128,6 +135,7 @@ adminWss.on('connection', (ws) => {
|
|||||||
// Handle close event
|
// Handle close event
|
||||||
ws.on('close', () => {
|
ws.on('close', () => {
|
||||||
onStatsWsClose(ws);
|
onStatsWsClose(ws);
|
||||||
|
onLogWsClose(ws);
|
||||||
adminClients.delete(ws);
|
adminClients.delete(ws);
|
||||||
logDebug('Admin', 'WebSocket client disconnected');
|
logDebug('Admin', 'WebSocket client disconnected');
|
||||||
});
|
});
|
||||||
@@ -136,6 +144,7 @@ adminWss.on('connection', (ws) => {
|
|||||||
ws.on('error', (err) => {
|
ws.on('error', (err) => {
|
||||||
logError('Admin', `WebSocket error: ${err.message}`);
|
logError('Admin', `WebSocket error: ${err.message}`);
|
||||||
onStatsWsClose(ws);
|
onStatsWsClose(ws);
|
||||||
|
onLogWsClose(ws);
|
||||||
adminClients.delete(ws);
|
adminClients.delete(ws);
|
||||||
try {
|
try {
|
||||||
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
||||||
@@ -186,6 +195,7 @@ function closeAllWebSockets() {
|
|||||||
}
|
}
|
||||||
adminClients.clear();
|
adminClients.clear();
|
||||||
closeStatsWebSocketState();
|
closeStatsWebSocketState();
|
||||||
|
closeLogWebSocketState();
|
||||||
|
|
||||||
// Stop health broadcasts
|
// Stop health broadcasts
|
||||||
stopHealthBroadcasts();
|
stopHealthBroadcasts();
|
||||||
|
|||||||
@@ -309,6 +309,46 @@
|
|||||||
Logs
|
Logs
|
||||||
<button onclick="openInfoModal('logs')" class="text-sm px-3 py-1 theme-button-info rounded transition-colors">Info</button>
|
<button onclick="openInfoModal('logs')" class="text-sm px-3 py-1 theme-button-info rounded transition-colors">Info</button>
|
||||||
</h2>
|
</h2>
|
||||||
|
<div class="flex flex-wrap items-center gap-3 mb-3">
|
||||||
|
<label for="log-channel-select" class="text-sm theme-text-secondary">Log file</label>
|
||||||
|
<select id="log-channel-select" onchange="onLogChannelChange()" class="theme-input rounded-lg px-3 py-2 text-sm min-w-[12rem]">
|
||||||
|
<option value="core">Core</option>
|
||||||
|
<option value="proxy">Internal Proxy</option>
|
||||||
|
<option value="dns">DNS</option>
|
||||||
|
<option value="plugins">Plugins</option>
|
||||||
|
<option value="holesail">Holesail</option>
|
||||||
|
</select>
|
||||||
|
<div class="flex-1 min-w-[16rem] max-w-2xl flex items-center gap-2">
|
||||||
|
<div class="relative flex-1 group">
|
||||||
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 theme-text-tertiary pointer-events-none group-focus-within:text-indigo-400 transition-colors">
|
||||||
|
<i class="fas fa-magnifying-glass text-sm" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
id="log-filter-input"
|
||||||
|
type="search"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
placeholder="Filter lines… text or /regex/flags"
|
||||||
|
class="w-full theme-input rounded-lg py-2 pl-9 pr-9 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
|
aria-label="Filter log lines"
|
||||||
|
oninput="onLogFilterInput()"
|
||||||
|
onkeydown="if (event.key === 'Escape') { clearLogFilter(); event.preventDefault(); }"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
id="log-filter-clear"
|
||||||
|
type="button"
|
||||||
|
class="hidden absolute right-1.5 top-1/2 -translate-y-1/2 p-1.5 rounded-md theme-text-tertiary hover:theme-text-primary hover:bg-white/10 transition-colors"
|
||||||
|
title="Clear filter (Esc)"
|
||||||
|
aria-label="Clear filter"
|
||||||
|
onclick="clearLogFilter()"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times text-sm" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span id="log-filter-stats" class="text-xs theme-text-tertiary tabular-nums whitespace-nowrap min-w-[5rem] text-right">—</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs theme-text-tertiary hidden lg:inline">Files under <code class="text-indigo-300">logs/</code> — live tail</span>
|
||||||
|
</div>
|
||||||
<div id="terminal" class="bg-black rounded-lg overflow-hidden h-96"></div>
|
<div id="terminal" class="bg-black rounded-lg overflow-hidden h-96"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -256,23 +256,23 @@ const infoContent = {
|
|||||||
},
|
},
|
||||||
'logs': {
|
'logs': {
|
||||||
title: 'System Logs',
|
title: 'System Logs',
|
||||||
description: 'View real-time system logs',
|
description: 'View and tail split log files under logs/',
|
||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
title: 'Overview',
|
title: 'Log files',
|
||||||
content: 'The Logs tab shows real-time logs from your P2NS instance. Logs are displayed in a terminal interface.'
|
content: 'Choose a log from the dropdown: core.log (main process), proxy.log (Internal Proxy), dns.log (DNS resolver), plugins.log (plugin SDK), holesail.log (Holesail). Noisy proxy and DNS traffic no longer appears in core.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Log Levels',
|
title: 'On disk',
|
||||||
content: 'Logs are color-coded by level: INFO (normal), WARN (yellow), ERROR (red), and DEBUG (gray).'
|
content: 'Each channel is appended to logs/<name>.log in the project directory (override with LOG_DIR). Files persist across restarts.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Log Buffer',
|
title: 'Live tail',
|
||||||
content: 'The terminal maintains a buffer of the most recent log messages. Older logs are automatically removed to manage memory.'
|
content: 'Selecting a log subscribes via WebSocket for new lines. On connect, the last ~1000 lines are loaded as a snapshot.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Real-time Updates',
|
title: 'Console',
|
||||||
content: 'Logs are updated in real-time via WebSocket connections. If the WebSocket is disconnected, the logs will pause until reconnection.'
|
content: 'Only core-channel logs are printed to the terminal console. Other channels are file + admin UI only.'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,180 @@
|
|||||||
// Logs UI functions - terminal rendering
|
// Logs UI — per-channel file tail via WebSocket + debounced filter
|
||||||
|
|
||||||
|
const LOG_CHANNEL_LABELS = {
|
||||||
|
core: 'Core',
|
||||||
|
proxy: 'Internal Proxy',
|
||||||
|
dns: 'DNS',
|
||||||
|
plugins: 'Plugins',
|
||||||
|
holesail: 'Holesail'
|
||||||
|
};
|
||||||
|
|
||||||
|
const LOG_FILTER_DEBOUNCE_MS = 200;
|
||||||
|
|
||||||
|
function getLogFilterDebounced() {
|
||||||
|
if (!window._logFilterDebounced) {
|
||||||
|
const debounce = window.sdk?.utils?.dom?.debounce;
|
||||||
|
window._logFilterDebounced = debounce
|
||||||
|
? debounce(() => {
|
||||||
|
if (window.activeTab === 'logs') {
|
||||||
|
paintLogTerminal(getActiveLogChannel());
|
||||||
|
}
|
||||||
|
}, LOG_FILTER_DEBOUNCE_MS)
|
||||||
|
: () => {
|
||||||
|
if (window.activeTab === 'logs') {
|
||||||
|
paintLogTerminal(getActiveLogChannel());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return window._logFilterDebounced;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveLogChannel() {
|
||||||
|
return window.activeLogChannel || 'core';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLogFilterQuery() {
|
||||||
|
const el = document.getElementById('log-filter-input');
|
||||||
|
return (el?.value || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match log line against filter (case-insensitive substring, or /pattern/flags regex).
|
||||||
|
*/
|
||||||
|
function lineMatchesLogFilter(line, query) {
|
||||||
|
if (!query) return true;
|
||||||
|
const text = String(line);
|
||||||
|
if (query.length >= 2 && query.startsWith('/')) {
|
||||||
|
const lastSlash = query.lastIndexOf('/');
|
||||||
|
if (lastSlash > 0) {
|
||||||
|
const pattern = query.slice(1, lastSlash);
|
||||||
|
const flags = query.slice(lastSlash + 1) || 'i';
|
||||||
|
try {
|
||||||
|
return new RegExp(pattern, flags).test(text);
|
||||||
|
} catch {
|
||||||
|
// invalid regex — fall through to substring
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text.toLowerCase().includes(query.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilteredLogLines(channel) {
|
||||||
|
const buf = getLogBuffer(channel);
|
||||||
|
const query = getLogFilterQuery();
|
||||||
|
if (!query) {
|
||||||
|
return { lines: buf, total: buf.length, shown: buf.length, query: '' };
|
||||||
|
}
|
||||||
|
const lines = buf.filter((line) => lineMatchesLogFilter(line, query));
|
||||||
|
return { lines, total: buf.length, shown: lines.length, query };
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLogFilterStats(channel) {
|
||||||
|
const el = document.getElementById('log-filter-stats');
|
||||||
|
if (!el) return;
|
||||||
|
const { total, shown, query } = getFilteredLogLines(channel);
|
||||||
|
if (!query) {
|
||||||
|
el.textContent = total === 0 ? '—' : `${total} line${total === 1 ? '' : 's'}`;
|
||||||
|
el.className = 'text-xs theme-text-tertiary tabular-nums whitespace-nowrap min-w-[5rem] text-right';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.textContent = `${shown} / ${total}`;
|
||||||
|
el.className = shown === 0
|
||||||
|
? 'text-xs text-amber-400 tabular-nums whitespace-nowrap min-w-[5rem] text-right font-medium'
|
||||||
|
: 'text-xs text-indigo-300 tabular-nums whitespace-nowrap min-w-[5rem] text-right font-medium';
|
||||||
|
el.title = `${shown} of ${total} lines match “${query}”`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLogFilterClearButton() {
|
||||||
|
const btn = document.getElementById('log-filter-clear');
|
||||||
|
if (!btn) return;
|
||||||
|
btn.classList.toggle('hidden', !getLogFilterQuery());
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLogFilterInput() {
|
||||||
|
updateLogFilterClearButton();
|
||||||
|
getLogFilterDebounced()();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLogFilter() {
|
||||||
|
const input = document.getElementById('log-filter-input');
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
updateLogFilterClearButton();
|
||||||
|
paintLogTerminal(getActiveLogChannel());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLogBuffer(channel) {
|
||||||
|
if (!window.logBuffers) window.logBuffers = new Map();
|
||||||
|
if (!window.logBuffers.has(channel)) {
|
||||||
|
window.logBuffers.set(channel, []);
|
||||||
|
}
|
||||||
|
return window.logBuffers.get(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushLogLine(channel, line) {
|
||||||
|
const buf = getLogBuffer(channel);
|
||||||
|
buf.push(line);
|
||||||
|
const max = window.maxLogLines || 1000;
|
||||||
|
if (buf.length > max) buf.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
function writelnToTerminal(line, channel) {
|
||||||
|
if (!window.term) return;
|
||||||
|
const ch = channel || getActiveLogChannel();
|
||||||
|
if (!lineMatchesLogFilter(line, getLogFilterQuery())) return;
|
||||||
|
window.term.writeln(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintLogTerminal(channel) {
|
||||||
|
if (!window.term) return;
|
||||||
|
window.term.reset();
|
||||||
|
const { lines } = getFilteredLogLines(channel);
|
||||||
|
lines.forEach((line) => window.term.writeln(line));
|
||||||
|
if (window.fitAddon) window.fitAddon.fit();
|
||||||
|
updateLogFilterStats(channel);
|
||||||
|
updateLogFilterClearButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribeLogChannel(channel) {
|
||||||
|
window.activeLogChannel = channel;
|
||||||
|
if (window.ws && window.ws.readyState === WebSocket.OPEN) {
|
||||||
|
window.ws.send(JSON.stringify({ type: 'subscribe-log', channel, lines: window.maxLogLines || 1000 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLogChannelChange() {
|
||||||
|
const select = document.getElementById('log-channel-select');
|
||||||
|
const channel = select?.value || 'core';
|
||||||
|
subscribeLogChannel(channel);
|
||||||
|
paintLogTerminal(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLogChannels() {
|
||||||
|
const select = document.getElementById('log-channel-select');
|
||||||
|
if (!select) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/logs');
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
const channels = data.channels || [];
|
||||||
|
select.innerHTML = channels
|
||||||
|
.map((c) => `<option value="${c.id}">${c.label || LOG_CHANNEL_LABELS[c.id] || c.id}</option>`)
|
||||||
|
.join('');
|
||||||
|
const preferred = window.activeLogChannel || 'core';
|
||||||
|
if (channels.some((c) => c.id === preferred)) {
|
||||||
|
select.value = preferred;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load log channels:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderLogs() {
|
function renderLogs() {
|
||||||
if (!window.terminalInitialized) {
|
if (!window.terminalInitialized) {
|
||||||
// Check if Terminal is available (from xterm.js CDN)
|
|
||||||
if (typeof Terminal === 'undefined' || typeof FitAddon === 'undefined') {
|
if (typeof Terminal === 'undefined' || typeof FitAddon === 'undefined') {
|
||||||
console.error('Terminal or FitAddon not loaded. Make sure xterm.js scripts are loaded.');
|
console.error('Terminal or FitAddon not loaded.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.term = new Terminal();
|
window.term = new Terminal();
|
||||||
@@ -18,18 +189,33 @@ function renderLogs() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (window.term) {
|
|
||||||
window.term.reset();
|
loadLogChannels().then(() => {
|
||||||
if (window.logBuffer && window.logBuffer.length > 0) {
|
const channel = getActiveLogChannel();
|
||||||
window.logBuffer.forEach(line => window.term.writeln(line));
|
const select = document.getElementById('log-channel-select');
|
||||||
}
|
if (select) select.value = channel;
|
||||||
if (window.fitAddon) {
|
subscribeLogChannel(channel);
|
||||||
window.fitAddon.fit();
|
paintLogTerminal(channel);
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLogSnapshot(data) {
|
||||||
|
if (!data?.channel || !Array.isArray(data.lines)) return;
|
||||||
|
window.logBuffers.set(data.channel, data.lines.slice(-(window.maxLogLines || 1000)));
|
||||||
|
if (window.activeTab === 'logs' && data.channel === getActiveLogChannel()) {
|
||||||
|
paintLogTerminal(data.channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFileLog(data) {
|
||||||
|
if (!data?.channel || !data.message) return;
|
||||||
|
pushLogLine(data.channel, data.message);
|
||||||
|
if (window.activeTab === 'logs' && data.channel === getActiveLogChannel()) {
|
||||||
|
writelnToTerminal(data.message, data.channel);
|
||||||
|
updateLogFilterStats(data.channel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle window resize for terminal
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
if (window.activeTab === 'logs' && window.fitAddon) {
|
if (window.activeTab === 'logs' && window.fitAddon) {
|
||||||
window.fitAddon.fit();
|
window.fitAddon.fit();
|
||||||
@@ -41,4 +227,9 @@ window.addEventListener('resize', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.renderLogs = renderLogs;
|
window.renderLogs = renderLogs;
|
||||||
|
window.onLogChannelChange = onLogChannelChange;
|
||||||
|
window.onLogFilterInput = onLogFilterInput;
|
||||||
|
window.clearLogFilter = clearLogFilter;
|
||||||
|
window.applyLogSnapshot = applyLogSnapshot;
|
||||||
|
window.applyFileLog = applyFileLog;
|
||||||
|
window.subscribeLogChannel = subscribeLogChannel;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ window.wsReconnectAttempts = 0;
|
|||||||
window.wsPollingInterval = null;
|
window.wsPollingInterval = null;
|
||||||
window.statusUpdateInterval = null;
|
window.statusUpdateInterval = null;
|
||||||
window.logBuffer = [];
|
window.logBuffer = [];
|
||||||
|
window.logBuffers = new Map();
|
||||||
|
window.activeLogChannel = 'core';
|
||||||
window.maxLogLines = 1000;
|
window.maxLogLines = 1000;
|
||||||
window.terminalInitialized = false;
|
window.terminalInitialized = false;
|
||||||
window.holesailLogBuffers = new Map();
|
window.holesailLogBuffers = new Map();
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ function connectWebSocket() {
|
|||||||
if (window.activeTab === 'stats' && window.subscribeStatsWebSocket) {
|
if (window.activeTab === 'stats' && window.subscribeStatsWebSocket) {
|
||||||
window.subscribeStatsWebSocket();
|
window.subscribeStatsWebSocket();
|
||||||
}
|
}
|
||||||
|
if (window.activeTab === 'logs' && window.subscribeLogChannel) {
|
||||||
|
window.subscribeLogChannel(window.activeLogChannel || 'core');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.ws.onclose = () => {
|
window.ws.onclose = () => {
|
||||||
@@ -239,11 +242,13 @@ function connectWebSocket() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (data.type === 'log-snapshot' && window.applyLogSnapshot) {
|
||||||
|
window.applyLogSnapshot(data);
|
||||||
|
} else if (data.type === 'file-log' && window.applyFileLog) {
|
||||||
|
window.applyFileLog(data);
|
||||||
} else if (data.type === 'log') {
|
} else if (data.type === 'log') {
|
||||||
window.logBuffer.push(`[${data.level.toUpperCase()}] ${data.message}`);
|
if (window.applyFileLog) {
|
||||||
if (window.logBuffer.length > window.maxLogLines) window.logBuffer.shift();
|
window.applyFileLog({ channel: 'core', level: data.level, message: data.message });
|
||||||
if (window.activeTab === 'logs' && window.term) {
|
|
||||||
window.term.writeln(`[${data.level.toUpperCase()}] ${data.message}`);
|
|
||||||
}
|
}
|
||||||
} else if (data.type === 'holesail-log') {
|
} else if (data.type === 'holesail-log') {
|
||||||
let buffer = window.holesailLogBuffers.get(data.id) || [];
|
let buffer = window.holesailLogBuffers.get(data.id) || [];
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ window.wsReconnectAttempts = 0;
|
|||||||
window.wsPollingInterval = null;
|
window.wsPollingInterval = null;
|
||||||
window.statusUpdateInterval = null;
|
window.statusUpdateInterval = null;
|
||||||
window.logBuffer = [];
|
window.logBuffer = [];
|
||||||
|
window.logBuffers = new Map();
|
||||||
|
window.activeLogChannel = 'core';
|
||||||
window.maxLogLines = 1000;
|
window.maxLogLines = 1000;
|
||||||
window.terminalInitialized = false;
|
window.terminalInitialized = false;
|
||||||
window.term = null;
|
window.term = null;
|
||||||
|
|||||||
@@ -195,11 +195,13 @@ function connectWebSocket() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (data.type === 'log-snapshot' && window.applyLogSnapshot) {
|
||||||
|
window.applyLogSnapshot(data);
|
||||||
|
} else if (data.type === 'file-log' && window.applyFileLog) {
|
||||||
|
window.applyFileLog(data);
|
||||||
} else if (data.type === 'log') {
|
} else if (data.type === 'log') {
|
||||||
window.logBuffer.push(`[${data.level.toUpperCase()}] ${data.message}`);
|
if (window.applyFileLog) {
|
||||||
if (window.logBuffer.length > window.maxLogLines) window.logBuffer.shift();
|
window.applyFileLog({ channel: 'core', level: data.level, message: data.message });
|
||||||
if (window.activeTab === 'logs' && window.term) {
|
|
||||||
window.term.writeln(`[${data.level.toUpperCase()}] ${data.message}`);
|
|
||||||
}
|
}
|
||||||
} else if (data.type === 'holesail-log') {
|
} else if (data.type === 'holesail-log') {
|
||||||
let buffer = window.holesailLogBuffers.get(data.id) || [];
|
let buffer = window.holesailLogBuffers.get(data.id) || [];
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
/**
|
||||||
|
* Multi-file logging: routes by prefix to logs/*.log with in-memory tail buffers.
|
||||||
|
*/
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const LOG_LEVELS = {
|
||||||
|
DEBUG: 0,
|
||||||
|
INFO: 1,
|
||||||
|
WARN: 2,
|
||||||
|
ERROR: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
const LEVEL_NAMES = ['DEBUG', 'INFO', 'WARN', 'ERROR'];
|
||||||
|
|
||||||
|
const LOG_CHANNELS = {
|
||||||
|
core: { file: 'core.log', label: 'Core', console: true },
|
||||||
|
proxy: { file: 'proxy.log', label: 'Internal Proxy', console: false },
|
||||||
|
dns: { file: 'dns.log', label: 'DNS', console: false },
|
||||||
|
plugins: { file: 'plugins.log', label: 'Plugins', console: false },
|
||||||
|
holesail: { file: 'holesail.log', label: 'Holesail', console: false }
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_BUFFER_LINES = parseInt(process.env.LOG_BUFFER_LINES || '2000', 10);
|
||||||
|
const logListeners = new Set();
|
||||||
|
|
||||||
|
const buffers = new Map();
|
||||||
|
const streams = new Map();
|
||||||
|
let logDir = null;
|
||||||
|
let minLevel = parseInt(process.env.LOG_LEVEL || '0', 10);
|
||||||
|
|
||||||
|
function getLogDir() {
|
||||||
|
if (!logDir) {
|
||||||
|
logDir = path.resolve(process.env.LOG_DIR || path.join(process.cwd(), 'logs'));
|
||||||
|
fs.mkdirSync(logDir, { recursive: true });
|
||||||
|
}
|
||||||
|
return logDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveChannel(prefix) {
|
||||||
|
if (!prefix) return 'core';
|
||||||
|
if (prefix === 'Internal Proxy') return 'proxy';
|
||||||
|
if (prefix === 'DNS') return 'dns';
|
||||||
|
if (prefix === 'Holesail' || prefix.startsWith('Holesail')) return 'holesail';
|
||||||
|
if (
|
||||||
|
prefix.startsWith('Plugin:') ||
|
||||||
|
prefix === 'PluginSDK' ||
|
||||||
|
prefix === 'PluginChannels' ||
|
||||||
|
prefix === 'PluginEvents'
|
||||||
|
) {
|
||||||
|
return 'plugins';
|
||||||
|
}
|
||||||
|
return 'core';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStream(channelId) {
|
||||||
|
if (streams.has(channelId)) return streams.get(channelId);
|
||||||
|
const meta = LOG_CHANNELS[channelId];
|
||||||
|
if (!meta) return null;
|
||||||
|
const filePath = path.join(getLogDir(), meta.file);
|
||||||
|
const stream = fs.createWriteStream(filePath, { flags: 'a' });
|
||||||
|
stream.on('error', (err) => {
|
||||||
|
console.error(`[P2NS] Log stream error (${channelId}): ${err.message}`);
|
||||||
|
});
|
||||||
|
streams.set(channelId, stream);
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendToBuffer(channelId, line) {
|
||||||
|
if (!buffers.has(channelId)) buffers.set(channelId, []);
|
||||||
|
const buf = buffers.get(channelId);
|
||||||
|
buf.push(line);
|
||||||
|
if (buf.length > MAX_BUFFER_LINES) buf.splice(0, buf.length - MAX_BUFFER_LINES);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLine(level, prefix, message) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const levelStr = LEVEL_NAMES[level] || 'INFO';
|
||||||
|
return `${timestamp} [${levelStr}] [${prefix}] ${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyListeners(channelId, level, line) {
|
||||||
|
for (const fn of logListeners) {
|
||||||
|
try {
|
||||||
|
fn(channelId, level, line);
|
||||||
|
} catch (_) {
|
||||||
|
/* ignore listener errors */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeLog(level, prefix, message) {
|
||||||
|
if (level < minLevel) return;
|
||||||
|
|
||||||
|
const channelId = resolveChannel(prefix);
|
||||||
|
const line = formatLine(level, prefix, message);
|
||||||
|
const stream = getStream(channelId);
|
||||||
|
if (stream && !stream.destroyed) {
|
||||||
|
stream.write(`${line}\n`);
|
||||||
|
}
|
||||||
|
appendToBuffer(channelId, line);
|
||||||
|
notifyListeners(channelId, LEVEL_NAMES[level].toLowerCase(), line);
|
||||||
|
|
||||||
|
const meta = LOG_CHANNELS[channelId];
|
||||||
|
if (meta?.console) {
|
||||||
|
return { channelId, level, line, toConsole: true };
|
||||||
|
}
|
||||||
|
return { channelId, level, line, toConsole: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLogLine(listener) {
|
||||||
|
logListeners.add(listener);
|
||||||
|
return () => logListeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
function listLogChannels() {
|
||||||
|
return Object.entries(LOG_CHANNELS).map(([id, meta]) => ({
|
||||||
|
id,
|
||||||
|
label: meta.label,
|
||||||
|
file: meta.file
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLogTail(channelId, lines = 500) {
|
||||||
|
const buf = buffers.get(channelId);
|
||||||
|
if (!buf || buf.length === 0) {
|
||||||
|
const meta = LOG_CHANNELS[channelId];
|
||||||
|
if (!meta) return [];
|
||||||
|
const filePath = path.join(getLogDir(), meta.file);
|
||||||
|
if (!fs.existsSync(filePath)) return [];
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const all = content.split('\n').filter(Boolean);
|
||||||
|
return all.slice(-lines);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf.slice(-lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMinLogLevel(level) {
|
||||||
|
minLevel = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLogStreams() {
|
||||||
|
for (const stream of streams.values()) {
|
||||||
|
try {
|
||||||
|
stream.end();
|
||||||
|
} catch (_) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
streams.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
LOG_LEVELS,
|
||||||
|
LOG_CHANNELS,
|
||||||
|
resolveChannel,
|
||||||
|
writeLog,
|
||||||
|
onLogLine,
|
||||||
|
listLogChannels,
|
||||||
|
getLogTail,
|
||||||
|
setMinLogLevel,
|
||||||
|
closeLogStreams,
|
||||||
|
getLogDir
|
||||||
|
};
|
||||||
@@ -1,74 +1,77 @@
|
|||||||
const HolesailLogger = require('holesail-logger');
|
const HolesailLogger = require('holesail-logger');
|
||||||
// Define LOG_LEVELS explicitly since they may not be accessible from the logger instance
|
const logFiles = require('./log-files');
|
||||||
const LOG_LEVELS = {
|
|
||||||
DEBUG: 0,
|
const LOG_LEVELS = logFiles.LOG_LEVELS;
|
||||||
INFO: 1,
|
|
||||||
WARN: 2,
|
|
||||||
ERROR: 3
|
|
||||||
};
|
|
||||||
// Create a singleton instance of HolesailLogger with minLevel from .env
|
|
||||||
let logger;
|
let logger;
|
||||||
try {
|
try {
|
||||||
logger = new HolesailLogger({
|
logger = new HolesailLogger({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
prefix: 'P2NS',
|
prefix: 'P2NS',
|
||||||
level: parseInt(process.env.LOG_LEVEL || 0)
|
level: parseInt(process.env.LOG_LEVEL || '0', 10)
|
||||||
});
|
});
|
||||||
// Test logger initialization
|
// Routed via emit after module load
|
||||||
logger.log({ type: LOG_LEVELS.INFO, msg: 'Logger initialized successfully' });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to initialize HolesailLogger: ${error.message}`);
|
console.error(`Failed to initialize HolesailLogger: ${error.message}`);
|
||||||
// Fallback to console-based logging
|
logger = null;
|
||||||
logger = {
|
}
|
||||||
log: ({ type, msg }) => {
|
|
||||||
const timestamp = new Date().toISOString();
|
function emit(level, prefix, message) {
|
||||||
const levelStr = type === 0 ? 'DEBUG' : type === 1 ? 'INFO' : type === 2 ? 'WARN' : type === 3 ? 'ERROR' : 'UNKNOWN';
|
const result = logFiles.writeLog(level, prefix, message);
|
||||||
const consoleMethod = type === 2 ? console.warn : type === 3 ? console.error : console.log;
|
if (!result?.toConsole) return;
|
||||||
consoleMethod(`${timestamp} [P2NS] [${levelStr}] ${msg}`);
|
|
||||||
}
|
const msg = `[${prefix}] ${message}`;
|
||||||
};
|
if (logger) {
|
||||||
}
|
logger.log({ type: level, msg });
|
||||||
// Wrapper functions to support prefix parameter and correct log level
|
return;
|
||||||
function logDebug(prefix, message) {
|
}
|
||||||
logger.log({ type: LOG_LEVELS.DEBUG, msg: `[${prefix}] ${message}` });
|
const levelStr = LOG_LEVELS.DEBUG === level ? 'DEBUG' : LOG_LEVELS.INFO === level ? 'INFO' : LOG_LEVELS.WARN === level ? 'WARN' : 'ERROR';
|
||||||
}
|
const consoleMethod = level === LOG_LEVELS.WARN ? console.warn : level === LOG_LEVELS.ERROR ? console.error : console.log;
|
||||||
function logInfo(prefix, message) {
|
consoleMethod(`${new Date().toISOString()} [P2NS] [${levelStr}] ${msg}`);
|
||||||
logger.log({ type: LOG_LEVELS.INFO, msg: `[${prefix}] ${message}` });
|
}
|
||||||
}
|
|
||||||
function logWarn(prefix, message) {
|
function logDebug(prefix, message) {
|
||||||
logger.log({ type: LOG_LEVELS.WARN, msg: `[${prefix}] ${message}` });
|
emit(LOG_LEVELS.DEBUG, prefix, message);
|
||||||
}
|
}
|
||||||
function logError(prefix, message) {
|
|
||||||
logger.log({ type: LOG_LEVELS.ERROR, msg: `[${prefix}] ${message}` });
|
function logInfo(prefix, message) {
|
||||||
|
emit(LOG_LEVELS.INFO, prefix, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logWarn(prefix, message) {
|
||||||
|
emit(LOG_LEVELS.WARN, prefix, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logError(prefix, message) {
|
||||||
|
emit(LOG_LEVELS.ERROR, prefix, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update log level at runtime
|
|
||||||
* @param {number} newLevel - New log level (0-3)
|
|
||||||
*/
|
|
||||||
function updateLogLevel(newLevel) {
|
function updateLogLevel(newLevel) {
|
||||||
|
logFiles.setMinLogLevel(newLevel);
|
||||||
try {
|
try {
|
||||||
// Recreate logger with new level
|
|
||||||
logger = new HolesailLogger({
|
logger = new HolesailLogger({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
prefix: 'P2NS',
|
prefix: 'P2NS',
|
||||||
level: newLevel
|
level: newLevel
|
||||||
});
|
});
|
||||||
logger.log({ type: LOG_LEVELS.INFO, msg: `Log level updated to ${newLevel}` });
|
logInfo('Main', `Log level updated to ${newLevel}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to update logger level: ${error.message}`);
|
console.error(`Failed to update logger level: ${error.message}`);
|
||||||
// Fallback logger
|
logger = null;
|
||||||
logger = {
|
|
||||||
log: ({ type, msg }) => {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
const levelStr = type === 0 ? 'DEBUG' : type === 1 ? 'INFO' : type === 2 ? 'WARN' : type === 3 ? 'ERROR' : 'UNKNOWN';
|
|
||||||
const consoleMethod = type === 2 ? console.warn : type === 3 ? console.error : console.log;
|
|
||||||
if (type >= newLevel) {
|
|
||||||
consoleMethod(`${timestamp} [P2NS] [${levelStr}] ${msg}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { logDebug, logInfo, logWarn, logError, updateLogLevel };
|
module.exports = {
|
||||||
|
logDebug,
|
||||||
|
logInfo,
|
||||||
|
logWarn,
|
||||||
|
logError,
|
||||||
|
updateLogLevel,
|
||||||
|
onLogLine: logFiles.onLogLine,
|
||||||
|
listLogChannels: logFiles.listLogChannels,
|
||||||
|
getLogTail: logFiles.getLogTail
|
||||||
|
};
|
||||||
|
|
||||||
|
if (logger) {
|
||||||
|
emit(LOG_LEVELS.INFO, 'Main', 'Logger initialized successfully');
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user