log-stream.js 3.1 KB

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