logbeacon
Version:
浏览器端日志采集与上报工具,支持多种日志服务后端,包括阿里云日志服务(SLS)和Grafana Loki
497 lines (430 loc) • 19.4 kB
JavaScript
/**
* 日志级别和关键字过滤系统
* 支持设置日志级别和关键字过滤
* 日志级别:trace < debug < info < warn < error < silent
* 关键字过滤:输入关键字,日志如果是关键字开头则打印,否则不打印
*/
(function() {
// 判断是否为开发环境
const isDev =
window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1";
// 日志级别常量
const LOG_LEVELS = {
TRACE: "trace",
DEBUG: "debug",
INFO: "info",
WARN: "warn",
ERROR: "error",
SILENT: "silent",
};
// 本地存储键名
const STORAGE_KEYS = {
LOG_LEVEL: "loglevel",
FILTER_KEYWORDS: "_logFilterKeyWords",
CONTROL_POSITION: "_logFilterControlPosition", // Using underscore prefix and pixel values
};
/**
* 创建日志控制面板
*/
function createLogFilterControl() {
// 只在开发环境显示控制面板
if (!isDev) {
return;
}
const dragThreshold = 5; // Pixels - Threshold to distinguish click from drag
// Helper function to apply styles
function applyStyles(element, styles) {
for (const property in styles) {
element.style[property] = styles[property];
}
}
// Helper function to calculate clamped position within viewport
function getClampedPosition(currentX, currentY, elementWidth, elementHeight, viewportWidth, viewportHeight) {
const x = Math.max(0, Math.min(currentX, viewportWidth - elementWidth));
const y = Math.max(0, Math.min(currentY, viewportHeight - elementHeight));
// Ensure values are not NaN, default to 0 if they are
return { x: isNaN(x) ? 0 : x, y: isNaN(y) ? 0 : y };
}
const svgIcon = `<svg t="1749970738262" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8631" width="24" height="24"><path d="M653.88544 0a92.16 92.16 0 0 1 65.09568 26.9312l187.61728 187.21792a92.16 92.16 0 0 1 27.05408 65.2288v120.6272A80.10752 80.10752 0 0 1 1013.76 480.09216v319.7952a80.10752 80.10752 0 0 1-77.55776 80.06656l-2.54976 0.03072v48.00512c0 53.02272-43.02848 96-96.12288 96H196.7104c-53.0944 0-96.12288-42.97728-96.12288-96v-48.00512A80.10752 80.10752 0 0 1 20.48 799.8976v-319.7952a80.10752 80.10752 0 0 1 77.55776-80.06656l2.54976-0.04096V96C100.58752 42.97728 143.616 0 196.7104 0h457.17504zM837.5296 879.99488H196.7104v17.28512a30.72 30.72 0 0 0 30.72 30.72h579.3792a30.72 30.72 0 0 0 30.72-30.72v-17.28512zM504.832 555.78624c-15.89248 0-30.03392 3.11296-42.41408 9.33888-12.36992 6.22592-23.1424 15.63648-32.28672 28.23168-5.69344 7.80288-10.10688 16.9984-13.2096 27.56608a116.13184 116.13184 0 0 0-4.66944 32.96256c0 21.74976 6.2464 38.7584 18.72896 51.01568 12.4928 12.26752 29.87008 18.40128 52.14208 18.40128 14.91968 0 28.55936-3.06176 40.88832-9.17504 12.3392-6.11328 22.86592-14.98112 31.55968-26.60352 6.144-8.32512 10.98752-18.05312 14.51008-29.19424 3.5328-11.14112 5.29408-22.44608 5.29408-33.91488 0-21.52448-6.22592-38.3488-18.67776-50.46272-12.45184-12.11392-29.73696-18.16576-51.8656-18.16576z m193.28-0.22528c-18.83136 0-34.80576 2.3552-47.9232 7.08608a88.22784 88.22784 0 0 0-34.31424 22.38464 79.59552 79.59552 0 0 0-17.7152 27.72992 94.8224 94.8224 0 0 0-6.02112 34.03776c0 24.00256 7.1168 42.63936 21.37088 55.9104 14.25408 13.27104 34.304 19.9168 60.19072 19.9168 10.72128 0 20.5824-0.65536 29.5936-1.97632a150.016 150.016 0 0 0 25.41568-5.89824l18.90304-88.43264h-66.60096L674.816 655.36h26.3168l-7.19872 33.41312a89.8048 89.8048 0 0 1-9.00096 1.85344c-2.7648 0.4096-5.46816 0.6144-8.0896 0.6144-12.9024 0-23.04-3.8912-30.38208-11.69408-7.35232-7.80288-11.02848-18.56512-11.02848-32.28672 0-18.83136 6.00064-33.6384 18.00192-44.4416 12.00128-10.79296 28.5696-16.19968 49.72544-16.19968 6.97344 0 13.6704 0.80896 20.08064 2.41664a80.86528 80.86528 0 0 1 18.62656 7.26016l9.216-32.84992a146.4832 146.4832 0 0 0-25.31328-5.95968 192.45056 192.45056 0 0 0-27.66848-1.91488z m-344.3712 3.93216h-43.07968L276.57216 719.36h108.9024l6.9632-31.8464h-66.14016l27.4432-128.02048z m148.2752 25.98912c9.30816 0 16.50688 3.19488 21.6064 9.56416 5.09952 6.37952 7.64928 15.4112 7.64928 27.11552 0 8.17152-1.15712 16.57856-3.4816 25.1904-2.33472 8.63232-5.36576 15.91296-9.1136 21.83168-5.09952 7.7312-10.42432 13.44512-15.9744 17.16224a31.92832 31.92832 0 0 1-18.1248 5.56032c-8.99072 0-16.0768-3.23584-21.25824-9.728-5.1712-6.48192-7.76192-15.36-7.76192-26.60352 0-8.25344 1.16736-16.71168 3.4816-25.37472 2.33472-8.66304 5.376-16.0256 9.1136-22.09792 4.58752-7.43424 9.73824-13.056 15.47264-16.87552a32.5632 32.5632 0 0 1 18.40128-5.7344zM608.34816 88.32H227.4304a30.72 30.72 0 0 0-30.72 30.72v280.95488h640.8192v-81.5616H700.52864c-50.8928-0.01024-92.14976-41.2672-92.16-92.16l-0.03072-137.95328z m96.12288 59.84256v58.91072a15.36 15.36 0 0 0 15.36 15.36h58.0848l-74.4448-74.27072z" fill="#FF6B08" p-id="8632"></path></svg>`;
// Create button element
const button = document.createElement("div");
button.id = "logFilterControl";
button.innerHTML = svgIcon;
applyStyles(button, {
position: "fixed",
width: "40px",
height: "40px",
backgroundColor: "#f0f0f0", // Light grey background
borderRadius: "50%",
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: "10000", // High z-index to stay on top
boxShadow: "0 2px 5px rgba(0,0,0,0.2)",
userSelect: "none", // Prevent text selection
border: "1px solid #ccc" // Subtle border
});
// Function to ensure button is within viewport
const ensureButtonInViewport = () => {
const buttonWidth = button.offsetWidth;
const buttonHeight = button.offsetHeight;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let currentLeft = parseFloat(button.style.left);
let currentTop = parseFloat(button.style.top);
if (isNaN(currentLeft) || isNaN(currentTop)) { // If position is not set via left/top (e.g., using bottom/right initially)
const rect = button.getBoundingClientRect();
currentLeft = rect.left;
currentTop = rect.top;
}
const { x: newLeft, y: newTop } = getClampedPosition(currentLeft, currentTop, buttonWidth, buttonHeight, viewportWidth, viewportHeight);
if (newLeft !== currentLeft || newTop !== currentTop) {
button.style.left = `${newLeft}px`;
button.style.top = `${newTop}px`;
button.style.bottom = 'auto';
button.style.right = 'auto';
// Re-save position if changed by viewport adjustment
sessionStorage.setItem(STORAGE_KEYS.CONTROL_POSITION, JSON.stringify({ left: button.style.left, top: button.style.top }));
}
};
// Load saved position or use default
const savedPosition = sessionStorage.getItem(STORAGE_KEYS.CONTROL_POSITION);
if (savedPosition) {
const { left, top } = JSON.parse(savedPosition);
button.style.left = left;
button.style.top = top;
} else {
button.style.bottom = "20px";
button.style.right = "20px";
}
ensureButtonInViewport(); // Ensure initial position is valid
// Draggable functionality
let dragging = false;
let wasDragged = false; // Flag to distinguish drag from click
let dragStartX, dragStartY;
let offsetX, offsetY;
// Function to handle clicks outside the panel to close it
let handleClickOutside;
const removeClickOutsideListener = () => {
if (handleClickOutside) {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
}
};
const addClickOutsideListener = () => {
removeClickOutsideListener(); // Remove any existing one first
handleClickOutside = (e) => {
if (panel.style.display === 'block' && !panel.contains(e.target) && !button.contains(e.target)) {
panel.style.display = 'none';
removeClickOutsideListener();
}
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
};
const startDrag = (event) => {
// Panel closing logic moved to drag() to avoid flicker on simple click
dragging = true;
wasDragged = false; // Reset flag
// Get the mouse cursor position at startup:
let clientX = event.clientX;
let clientY = event.clientY;
if (event.touches && event.touches.length > 0) {
clientX = event.touches[0].clientX;
clientY = event.touches[0].clientY;
}
dragStartX = clientX;
dragStartY = clientY;
offsetX = clientX - button.getBoundingClientRect().left;
offsetY = clientY - button.getBoundingClientRect().top;
button.style.cursor = 'grabbing';
document.body.style.userSelect = 'none';
};
const drag = (event) => {
if (!dragging) return;
event.preventDefault(); // Prevent page scrolling on touch devices
let clientX = event.clientX;
let clientY = event.clientY;
if (event.touches && event.touches.length > 0) {
clientX = event.touches[0].clientX;
clientY = event.touches[0].clientY;
}
const currentX = clientX - offsetX;
const currentY = clientY - offsetY;
const distX = clientX - dragStartX;
const distY = clientY - dragStartY;
// Only set wasDragged and close panel if actual movement beyond threshold occurred
if (!wasDragged && (Math.abs(distX) > dragThreshold || Math.abs(distY) > dragThreshold)) {
wasDragged = true;
// Close panel if it's open, now that we've confirmed it's a drag
if (panel.style.display === 'block') {
hidePanel(); // Use helper
}
}
if (wasDragged) {
const buttonWidth = button.offsetWidth;
const buttonHeight = button.offsetHeight;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const { x: boundedX, y: boundedY } = getClampedPosition(currentX, currentY, buttonWidth, buttonHeight, viewportWidth, viewportHeight);
button.style.left = `${boundedX}px`;
button.style.top = `${boundedY}px`;
button.style.right = 'auto'; // Ensure right/bottom are not interfering
button.style.bottom = 'auto';
button.style.cursor = 'grabbing';
document.body.style.userSelect = 'none'; // Prevent text selection during drag
}
};
const stopDrag = () => {
if (!dragging) return;
dragging = false;
button.style.cursor = 'pointer';
document.body.style.userSelect = '';
if (wasDragged) { // Only save if it was a drag
sessionStorage.setItem(STORAGE_KEYS.CONTROL_POSITION, JSON.stringify({ left: button.style.left, top: button.style.top }));
}
};
button.addEventListener('mousedown', startDrag);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', stopDrag);
button.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
startDrag(e);
}
}, { passive: false });
document.addEventListener('touchmove', drag, { passive: false });
document.addEventListener('touchend', stopDrag);
document.addEventListener('touchcancel', stopDrag);
// Add resize listener to adjust button position
window.addEventListener('resize', ensureButtonInViewport);
// Create panel element
const panel = document.createElement("div");
panel.id = "logFilterPanel";
applyStyles(panel, {
position: "fixed",
display: "none", // Initially hidden
width: "280px",
padding: "15px",
backgroundColor: "white",
border: "1px solid #ccc",
borderRadius: "5px",
boxShadow: "0 4px 8px rgba(0,0,0,0.1)",
zIndex: "10001", // Higher than button
fontFamily: "Arial, sans-serif",
fontSize: "14px",
color: "#333",
boxSizing: "border-box"
});
// 创建标题
const title = document.createElement("h3");
title.textContent = "日志过滤器设置";
applyStyles(title, {
marginTop: "0",
marginBottom: "15px",
fontSize: "16px",
color: "#333",
textAlign: "center",
borderBottom: "1px solid #eee",
paddingBottom: "10px"
});
panel.appendChild(title);
// 创建日志级别选择器
const levelLabel = document.createElement("label");
levelLabel.textContent = "日志级别:";
applyStyles(levelLabel, {
display: "block",
marginBottom: "5px",
fontWeight: "bold"
});
panel.appendChild(levelLabel);
const levels = Object.values(LOG_LEVELS);
const form = document.createElement("form");
const currentLevel = localStorage.getItem(STORAGE_KEYS.LOG_LEVEL) || LOG_LEVELS.INFO;
levels.forEach((level) => {
const label = document.createElement("label");
label.style.display = "block";
label.style.margin = "5px 0";
const radio = document.createElement("input");
radio.type = "radio";
radio.name = "loglevel";
radio.value = level;
radio.checked = level === currentLevel.toLowerCase();
radio.style.marginRight = "5px";
radio.onchange = () => {
if (radio.checked) {
const newLevel = level.toUpperCase();
localStorage.setItem(STORAGE_KEYS.LOG_LEVEL, newLevel);
setTimeout(() => {
panel.style.display = "none";
removeClickOutsideListener(); // Remove listener when panel closes
}, 200);
}
};
label.appendChild(radio);
label.appendChild(document.createTextNode(level));
form.appendChild(label);
});
panel.appendChild(form);
const keywordLabel = document.createElement("label");
keywordLabel.textContent = "关键字过滤:";
keywordLabel.setAttribute("for", "logKeywordInput");
applyStyles(keywordLabel, {
display: "block",
marginBottom: "5px",
fontWeight: "bold"
});
panel.appendChild(keywordLabel);
const keywordDesc = document.createElement("p");
keywordDesc.textContent = "日志如果以关键字开头则显示,否则不显示";
keywordDesc.style.margin = "0 0 5px 0";
keywordDesc.style.fontSize = "11px";
keywordDesc.style.color = "#666";
panel.appendChild(keywordDesc);
const keywordInput = document.createElement("input");
keywordInput.type = "text";
keywordInput.placeholder = "输入关键字";
keywordInput.value = localStorage.getItem(STORAGE_KEYS.FILTER_KEYWORDS) || "";
applyStyles(keywordInput, {
width: "100%",
padding: "5px",
marginBottom: "5px",
border: "1px solid #ddd",
borderRadius: "3px",
boxSizing: "border-box",
fontSize: "14px"
});
panel.appendChild(keywordInput);
const buttonContainer = document.createElement("div");
applyStyles(buttonContainer, {
display: "flex",
justifyContent: "space-between",
marginBottom: "10px"
});
const saveButton = document.createElement("button");
saveButton.textContent = "保存";
applyStyles(saveButton, {
background: "#28a745",
color: "white",
border: "none",
borderRadius: "3px",
padding: "5px 10px",
fontSize: "12px",
cursor: "pointer",
flex: "1",
marginRight: "5px"
});
const clearButton = document.createElement("button");
clearButton.textContent = "清除关键字";
applyStyles(clearButton, {
background: "#dc3545",
color: "white",
border: "none",
borderRadius: "3px",
padding: "5px 10px",
fontSize: "12px",
cursor: "pointer",
flex: "1",
marginLeft: "5px"
});
const saveKeyword = () => {
const keyword = keywordInput.value.trim();
localStorage.setItem(STORAGE_KEYS.FILTER_KEYWORDS, keyword);
};
// Helper function to hide the panel
const hidePanel = () => {
panel.style.display = "none";
removeClickOutsideListener();
};
// Helper function to show and position the panel
const showPanel = () => {
panel.style.display = "block";
const buttonRect = button.getBoundingClientRect();
const panelHeight = panel.offsetHeight;
const panelWidth = panel.offsetWidth;
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
let panelTop = buttonRect.top - panelHeight - 10;
if (panelTop < 0) {
panelTop = buttonRect.bottom + 10;
}
panelTop = Math.max(0, Math.min(panelTop, viewportHeight - panelHeight));
let panelLeft = buttonRect.left + (buttonRect.width / 2) - (panelWidth / 2);
panelLeft = Math.max(0, Math.min(panelLeft, viewportWidth - panelWidth));
applyStyles(panel, {
top: `${panelTop}px`,
left: `${panelLeft}px`,
bottom: 'auto',
right: 'auto'
});
addClickOutsideListener(); // Add listener when panel opens
};
const saveAndClose = () => {
saveKeyword();
hidePanel(); // Use helper
};
saveButton.onclick = saveAndClose;
clearButton.onclick = (e) => {
e.preventDefault();
keywordInput.value = "";
localStorage.removeItem(STORAGE_KEYS.FILTER_KEYWORDS);
hidePanel(); // Use helper
};
keywordInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
saveAndClose();
}
});
buttonContainer.appendChild(saveButton);
buttonContainer.appendChild(clearButton);
panel.appendChild(buttonContainer);
const info = document.createElement("p");
info.textContent = "设置将保存在浏览器本地存储中";
applyStyles(info, {
fontSize: "12px",
color: "#666",
marginTop: "15px"
});
panel.appendChild(info);
button.onclick = (e) => {
if (wasDragged) {
wasDragged = false; // Reset for next interaction
return; // Do not toggle panel if it was a drag
}
const isPanelVisible = panel.style.display === "block";
if (isPanelVisible) {
hidePanel(); // Use helper
} else {
showPanel(); // Use helper
}
};
document.body.appendChild(button);
document.body.appendChild(panel);
}
/**
* 初始化日志过滤系统
*/
function initLogFilter() {
// 检查文档是否已经加载完成
if (
document.readyState === "complete" ||
document.readyState === "interactive"
) {
setTimeout(createLogFilterControl, 500);
} else {
// 否则等待 DOMContentLoaded 事件
document.addEventListener("DOMContentLoaded", () => {
setTimeout(createLogFilterControl, 500);
});
}
// Helper function to calculate clamped position within viewport
function getClampedPosition(currentX, currentY, elementWidth, elementHeight, viewportWidth, viewportHeight) {
const x = Math.max(0, Math.min(currentX, viewportWidth - elementWidth));
const y = Math.max(0, Math.min(currentY, viewportHeight - elementHeight));
// Ensure values are not NaN, default to 0 if they are
return { x: isNaN(x) ? 0 : x, y: isNaN(y) ? 0 : y };
}
}
initLogFilter();
})();