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);
});