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