var logSource = null; var currentUnit = null; var logPid = 0; var MAX_LOG_LINES = 800; var MIN_LOG_LINES = 500; var logLines = []; function escapeHtml(s) { return s .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function colorLog(line) { var s = escapeHtml(line); var 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 (var i = 0; i < levels.length; i++) { var lvl = levels[i][0], cls = levels[i][1]; if (s.includes(lvl) || s.includes(lvl.toLowerCase())) { return '' + s + ""; } } return '' + s + ""; } function connectLog(unit, target) { var 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 = ""; var source = new EventSource("/api/service/log?unit=" + encodeURIComponent(unit)); logSource = source; source.onmessage = function (e) { if (source !== logSource) { return; } logLines.push(e.data); if (logLines.length > MAX_LOG_LINES) { logLines.shift(); } var line = document.createElement("div"); line.innerHTML = colorLog(e.data); var atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 20; el.appendChild(line); if (el.childElementCount > MAX_LOG_LINES) { while (el.childElementCount > MIN_LOG_LINES) { el.firstElementChild.remove(); } } if (atBottom) { requestAnimationFrame(function () { el.scrollTop = el.scrollHeight; }); } }; source.addEventListener("pid", function (e) { if (source !== logSource) { return; } logPid = Number(e.data); }); source.onerror = function () { if (source !== logSource) { return; } console.warn("[log-stream] error unit=" + unit + ", state=" + source.readyState); }; } function disconnectLog() { if (logSource) { logSource.close(); logSource = null; } if (logPid > 0) { fetch("/api/service/log/close?pid=" + logPid).catch(function () {}); logPid = 0; } currentUnit = null; logLines = []; } function getCurrentUnit() { return currentUnit; } function clearLog(target) { logLines = []; var el = typeof target === "string" ? document.getElementById(target) : target; if (el) { // replaceChildren 兼容兜底 (Chrome < 86 / Safari < 14) if (el.replaceChildren) { el.replaceChildren(); } else { while (el.firstChild) { el.removeChild(el.firstChild); } } el.scrollTop = 0; } } function copyLog(target) { var el = typeof target === "string" ? document.getElementById(target) : target; // 每行日志是一个独立的 div,textContent 直接拼接不会带换行,需逐行 join("\n") var text = el ? Array.prototype.map .call(el.children, function (c) { return c.textContent; }) .join("\n") : logLines.join("\n"); function fallbackCopy() { var ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); var ok = document.execCommand("copy"); ta.remove(); alert(ok ? "复制成功" : "复制失败"); } if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard .writeText(text) .then(function () { return alert("复制成功"); }) .catch(fallbackCopy); } else { fallbackCopy(); } } ["beforeunload", "pagehide"].forEach(function (e) { return window.addEventListener(e, disconnectLog); });