let logSource = null; let currentUnit = null; const MAX_LOG_LINES = 2000; let logLines = []; function colorLog(line) { let s = line .replace(/&/g, "&") .replace(//g, ">"); const levels = [ ["TRACE", "log-trace"], ["DEBUG", "log-debug"], ["INFO", "log-info"], ["WARN", "log-warn"], ["WARNING", "log-warn"], ["ERROR", "log-error"], ["FATAL", "log-fatal"], ["PANIC", "log-panic"] ]; for (const [level, cls] of levels) { if (s.includes(level) || s.includes(level.toLowerCase())) { return `${s}`; } } return s; } const monthMap = { Jan: '01', Feb: '02', Mar: '03', Apr: '04', May: '05', Jun: '06', Jul: '07', Aug: '08', Sep: '09', Oct: '10', Nov: '11', Dec: '12' }; function formatLog(line) { return line.replace( /^(\w{3})\s+(\d{1,2})\s+(\d{2}:\d{2}:\d{2})\s+\S+\s+[^\s]+\[\d+\]:\s+\[([a-z]+)\]:\s+\[[^\]]+\](.*)/i, (_, mon, day, time, level, msg) => `${monthMap[mon]}-${day.padStart(2, '0')} ${time} ${level.toUpperCase()}${msg}` ); } function connectLog(unit, target) { const el = typeof target === "string" ? document.getElementById(target) : target; if (!el) { console.error("[log-stream] target element not found"); return; } if (currentUnit === unit && logSource && logSource.readyState !== EventSource.CLOSED) { return; } disconnectLog(); currentUnit = unit; logLines = []; el.textContent = ""; const source = new EventSource( `/api/service/log?unit=${encodeURIComponent(unit)}` ); logSource = source; source.onmessage = (e) => { if (source !== logSource) { return; } logLines.push(e.data); if (logLines.length > MAX_LOG_LINES) { logLines.splice( 0, logLines.length - MAX_LOG_LINES ); } el.innerHTML = logLines .map(formatLog) .map(colorLog) .join("
") + "


"; el.scrollTop = el.scrollHeight; }; source.onerror = () => { if (source !== logSource) { return; } console.log( `[log-stream] error unit=${unit}, state=${source.readyState}` ); }; } function disconnectLog() { if (logSource) { logSource.close(); logSource = null; } currentUnit = null; logLines = []; } function getCurrentUnit() { return currentUnit; }