log-stream.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. let logSource = null;
  2. let currentUnit = null;
  3. let autoScroll = true;
  4. const MAX_LOG_LINES = 2000;
  5. let logLines = [];
  6. function escapeHtml(s) {
  7. return s
  8. .replace(/&/g, "&")
  9. .replace(/</g, "&lt;")
  10. .replace(/>/g, "&gt;");
  11. }
  12. function colorLog(line) {
  13. const s = escapeHtml(line);
  14. const levels = [
  15. ["TRACE", "log-trace"],
  16. ["DEBUG", "log-debug"],
  17. ["INFO", "log-info"],
  18. ["WARN", "log-warn"],
  19. ["WARNING", "log-warn"],
  20. ["ERROR", "log-error"],
  21. ["FATAL", "log-fatal"],
  22. ["PANIC", "log-panic"]
  23. ];
  24. for (const [lvl, cls] of levels) {
  25. if (s.includes(lvl) || s.includes(lvl.toLowerCase())) {
  26. return `<span class="${cls}">${s}</span>`;
  27. }
  28. }
  29. return `<span class="log-default">${s}</span>`;
  30. }
  31. function connectLog(unit, target) {
  32. const el = typeof target === "string"
  33. ? document.getElementById(target)
  34. : target;
  35. if (!el) {
  36. console.error("[log-stream] target element not found");
  37. return;
  38. }
  39. if (currentUnit === unit && logSource &&
  40. logSource.readyState !== EventSource.CLOSED) {
  41. return;
  42. }
  43. disconnectLog();
  44. currentUnit = unit;
  45. logLines = [];
  46. el.textContent = "";
  47. const source = new EventSource(
  48. `/api/service/log?unit=${encodeURIComponent(unit)}`
  49. );
  50. logSource = source;
  51. el.addEventListener("scroll", () => {
  52. const distance = el.scrollHeight - el.scrollTop - el.clientHeight;
  53. if (distance < 20) {
  54. autoScroll = true;
  55. } else {
  56. autoScroll = false;
  57. }
  58. });
  59. source.onmessage = (e) => {
  60. if (source !== logSource) {
  61. return;
  62. }
  63. logLines.push(e.data);
  64. if (logLines.length > MAX_LOG_LINES) {
  65. logLines.shift();
  66. }
  67. const line = document.createElement("div");
  68. line.innerHTML = colorLog(e.data);
  69. el.appendChild(line);
  70. while (el.childElementCount > MAX_LOG_LINES) {
  71. el.removeChild(el.firstElementChild);
  72. }
  73. if (autoScroll) {
  74. requestAnimationFrame(() => {
  75. el.scrollTop = el.scrollHeight;
  76. });
  77. }
  78. };
  79. source.onerror = () => {
  80. if (source !== logSource) {
  81. return;
  82. }
  83. console.warn(
  84. `[log-stream] error unit=${unit}, state=${source.readyState}`
  85. );
  86. };
  87. }
  88. function disconnectLog() {
  89. if (logSource) {
  90. logSource.close();
  91. logSource = null;
  92. }
  93. currentUnit = null;
  94. logLines = [];
  95. }
  96. function getCurrentUnit() {
  97. return currentUnit;
  98. }
  99. function clearLog(target) {
  100. logLines = [];
  101. const el = typeof target === "string"
  102. ? document.getElementById(target)
  103. : target;
  104. if (el) {
  105. el.replaceChildren();
  106. el.scrollTop = 0;
  107. }
  108. }
  109. function copyLog(target) {
  110. const text = logLines.join("\n");
  111. if (navigator.clipboard) {
  112. navigator.clipboard.writeText(text)
  113. .then(() => alert("复制成功"))
  114. .catch(() => alert("复制失败"));
  115. } else {
  116. const ta = document.createElement("textarea");
  117. ta.value = text;
  118. document.body.appendChild(ta);
  119. ta.select();
  120. document.execCommand("copy");
  121. ta.remove();
  122. alert("复制成功");
  123. }
  124. }