log-stream.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. var logSource = null;
  2. var currentUnit = null;
  3. var logPid = 0;
  4. var MAX_LOG_LINES = 800;
  5. var MIN_LOG_LINES = 500;
  6. var logLines = [];
  7. function escapeHtml(s) {
  8. return s
  9. .replace(/&/g, "&")
  10. .replace(/</g, "&lt;")
  11. .replace(/>/g, "&gt;")
  12. .replace(/"/g, "&quot;")
  13. .replace(/'/g, "&#39;");
  14. }
  15. function colorLog(line) {
  16. var s = escapeHtml(line);
  17. var levels = [
  18. ["TRACE", "log-trace"],
  19. ["DEBUG", "log-debug"],
  20. ["INFO", "log-info"],
  21. ["WARN", "log-warn"],
  22. ["WARNING", "log-warn"],
  23. ["ERROR", "log-error"],
  24. ["FATAL", "log-fatal"],
  25. ["PANIC", "log-panic"],
  26. ];
  27. for (var i = 0; i < levels.length; i++) {
  28. var lvl = levels[i][0],
  29. cls = levels[i][1];
  30. if (s.includes(lvl) || s.includes(lvl.toLowerCase())) {
  31. return '<span class="' + cls + '">' + s + "</span>";
  32. }
  33. }
  34. return '<span class="log-default">' + s + "</span>";
  35. }
  36. function connectLog(unit, target) {
  37. var el = typeof target === "string" ? document.getElementById(target) : target;
  38. if (!el) {
  39. console.error("[log-stream] target element not found");
  40. return;
  41. }
  42. if (currentUnit === unit && logSource && logSource.readyState !== EventSource.CLOSED) {
  43. return;
  44. }
  45. disconnectLog();
  46. currentUnit = unit;
  47. logLines = [];
  48. el.textContent = "";
  49. var source = new EventSource("/api/service/log?unit=" + encodeURIComponent(unit));
  50. logSource = source;
  51. source.onmessage = function (e) {
  52. if (source !== logSource) {
  53. return;
  54. }
  55. logLines.push(e.data);
  56. if (logLines.length > MAX_LOG_LINES) {
  57. logLines.shift();
  58. }
  59. var line = document.createElement("div");
  60. line.innerHTML = colorLog(e.data);
  61. var atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 20;
  62. el.appendChild(line);
  63. if (el.childElementCount > MAX_LOG_LINES) {
  64. while (el.childElementCount > MIN_LOG_LINES) {
  65. el.firstElementChild.remove();
  66. }
  67. }
  68. if (atBottom) {
  69. requestAnimationFrame(function () {
  70. el.scrollTop = el.scrollHeight;
  71. });
  72. }
  73. };
  74. source.addEventListener("pid", function (e) {
  75. if (source !== logSource) {
  76. return;
  77. }
  78. logPid = Number(e.data);
  79. });
  80. source.onerror = function () {
  81. if (source !== logSource) {
  82. return;
  83. }
  84. console.warn("[log-stream] error unit=" + unit + ", state=" + source.readyState);
  85. };
  86. }
  87. function disconnectLog() {
  88. if (logSource) {
  89. logSource.close();
  90. logSource = null;
  91. }
  92. if (logPid > 0) {
  93. fetch("/api/service/log/close?pid=" + logPid).catch(function () {});
  94. logPid = 0;
  95. }
  96. currentUnit = null;
  97. logLines = [];
  98. }
  99. function getCurrentUnit() {
  100. return currentUnit;
  101. }
  102. function clearLog(target) {
  103. logLines = [];
  104. var el = typeof target === "string" ? document.getElementById(target) : target;
  105. if (el) {
  106. // replaceChildren 兼容兜底 (Chrome < 86 / Safari < 14)
  107. if (el.replaceChildren) {
  108. el.replaceChildren();
  109. } else {
  110. while (el.firstChild) {
  111. el.removeChild(el.firstChild);
  112. }
  113. }
  114. el.scrollTop = 0;
  115. }
  116. }
  117. function copyLog(target) {
  118. var el = typeof target === "string" ? document.getElementById(target) : target;
  119. // 每行日志是一个独立的 div,textContent 直接拼接不会带换行,需逐行 join("\n")
  120. var text = el
  121. ? Array.prototype.map
  122. .call(el.children, function (c) {
  123. return c.textContent;
  124. })
  125. .join("\n")
  126. : logLines.join("\n");
  127. function fallbackCopy() {
  128. var ta = document.createElement("textarea");
  129. ta.value = text;
  130. ta.style.position = "fixed";
  131. ta.style.opacity = "0";
  132. document.body.appendChild(ta);
  133. ta.select();
  134. var ok = document.execCommand("copy");
  135. ta.remove();
  136. alert(ok ? "复制成功" : "复制失败");
  137. }
  138. if (navigator.clipboard && navigator.clipboard.writeText) {
  139. navigator.clipboard
  140. .writeText(text)
  141. .then(function () {
  142. return alert("复制成功");
  143. })
  144. .catch(fallbackCopy);
  145. } else {
  146. fallbackCopy();
  147. }
  148. }
  149. ["beforeunload", "pagehide"].forEach(function (e) {
  150. return window.addEventListener(e, disconnectLog);
  151. });