export const logsModule = { toggleLogsNavItem(show) { const logsNavItem = document.getElementById('logs-nav-item'); if (logsNavItem) { logsNavItem.style.display = show ? '' : 'none'; } }, async refreshLogs(incremental = false) { const logsContent = document.getElementById('logs-content'); if (!logsContent) return; try { if (incremental && !this.latestLogTimestamp) { incremental = false; } if (!incremental) { logsContent.innerHTML = '
' + i18n.t('logs.empty_title') + '
' + i18n.t('logs.empty_desc') + '
' + i18n.t('logs.empty_title') + '
' + i18n.t('logs.empty_desc') + '
' + i18n.t('logs.upgrade_required_desc') + '
' + i18n.t('logs.load_error') + '
' + error.message + '
' + i18n.t('logs.empty_title') + '
' + i18n.t('logs.empty_desc') + '
${this.buildLogsHtml(this.displayedLogLines)}
`;
if (scrollToBottom) {
const logsTextElement = logsContent.querySelector('.logs-text');
if (logsTextElement) {
logsTextElement.scrollTop = logsTextElement.scrollHeight;
}
}
},
appendLogs(newLines, totalLineCount) {
const logsContent = document.getElementById('logs-content');
if (!logsContent) return;
if (!newLines || newLines.length === 0) {
return;
}
const logsTextElement = logsContent.querySelector('.logs-text');
const logsInfoElement = logsContent.querySelector('.logs-info');
const filteredNewLines = newLines.filter(line => !line.includes('/v0/management/'));
if (filteredNewLines.length === 0) {
return;
}
if (!logsTextElement) {
this.renderLogs(filteredNewLines, totalLineCount || filteredNewLines.length, true);
return;
}
const isAtBottom = logsTextElement.scrollHeight - logsTextElement.scrollTop - logsTextElement.clientHeight < 50;
this.displayedLogLines = this.displayedLogLines.concat(filteredNewLines);
if (this.displayedLogLines.length > this.maxDisplayLogLines) {
this.displayedLogLines = this.displayedLogLines.slice(this.displayedLogLines.length - this.maxDisplayLogLines);
}
logsTextElement.innerHTML = this.buildLogsHtml(this.displayedLogLines);
if (logsInfoElement) {
const displayedLines = this.displayedLogLines.length;
logsInfoElement.innerHTML = ` ${displayedLines} ${i18n.t('logs.lines')}`;
}
if (isAtBottom) {
logsTextElement.scrollTop = logsTextElement.scrollHeight;
}
},
buildLogsHtml(lines) {
if (!lines || lines.length === 0) {
return '';
}
return lines.map(line => {
let processedLine = line.replace(/\[GIN\]\s+\d{4}\/\d{2}\/\d{2}\s+-\s+\d{2}:\d{2}:\d{2}\s+/g, '');
const highlights = [];
const statusInfo = this.detectHttpStatus(line);
if (statusInfo) {
const statusPattern = new RegExp(`\\b${statusInfo.code}\\b`);
const match = statusPattern.exec(processedLine);
if (match) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: `log-status-tag log-status-${statusInfo.bucket}`,
priority: 10
});
}
}
const timestampPattern = /\d{4}[-/]\d{2}[-/]\d{2}[T]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?|\[\d{2}:\d{2}:\d{2}\]/g;
let match;
while ((match = timestampPattern.exec(processedLine)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: 'log-timestamp',
priority: 5
});
}
const bracketTimestampPattern = /\[\d{4}[-/]\d{2}[-/]\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?\]/g;
while ((match = bracketTimestampPattern.exec(processedLine)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: 'log-timestamp',
priority: 5
});
}
const levelPattern = /\[(ERROR|ERRO|ERR|FATAL|CRITICAL|CRIT|WARN|WARNING|INFO|DEBUG|TRACE|PANIC)\]/gi;
while ((match = levelPattern.exec(processedLine)) !== null) {
const level = match[1].toUpperCase();
let className = 'log-level';
if (['ERROR', 'ERRO', 'ERR', 'FATAL', 'CRITICAL', 'CRIT', 'PANIC'].includes(level)) {
className += ' log-level-error';
} else if (['WARN', 'WARNING'].includes(level)) {
className += ' log-level-warn';
} else if (level === 'INFO') {
className += ' log-level-info';
} else if (['DEBUG', 'TRACE'].includes(level)) {
className += ' log-level-debug';
}
highlights.push({
start: match.index,
end: match.index + match[0].length,
className,
priority: 8
});
}
const methodPattern = /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)\b/g;
while ((match = methodPattern.exec(processedLine)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: 'log-http-method',
priority: 6
});
}
const urlPattern = /(https?:\/\/[^\s<>"']+)/g;
while ((match = urlPattern.exec(processedLine)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: 'log-path',
priority: 4
});
}
const ipPattern = /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g;
while ((match = ipPattern.exec(processedLine)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: 'log-ip',
priority: 7
});
}
const successPattern = /\b(success|successful|succeeded|completed|ok|done|passed)\b/gi;
while ((match = successPattern.exec(processedLine)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: 'log-keyword-success',
priority: 3
});
}
const errorPattern = /\b(failed|failure|error|exception|panic|fatal|critical|aborted|denied|refused|timeout|invalid)\b/gi;
while ((match = errorPattern.exec(processedLine)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: 'log-keyword-error',
priority: 3
});
}
const headersPattern = /\b(x-[a-z0-9-]+|authorization|content-type|user-agent)\b/gi;
while ((match = headersPattern.exec(processedLine)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
className: 'log-header-key',
priority: 2
});
}
highlights.sort((a, b) => {
if (a.start === b.start) {
return b.priority - a.priority;
}
return a.start - b.start;
});
let cursor = 0;
let result = '';
highlights.forEach((highlight) => {
if (highlight.start < cursor) {
return;
}
result += this.escapeHtml(processedLine.slice(cursor, highlight.start));
result += `${this.escapeHtml(processedLine.slice(highlight.start, highlight.end))}`;
cursor = highlight.end;
});
result += this.escapeHtml(processedLine.slice(cursor));
return `${result}`;
}).join('');
},
detectHttpStatus(line) {
if (!line) return null;
const patterns = [
/\|\s*([1-5]\d{2})\s*\|/,
/\b([1-5]\d{2})\s*-/,
/\b(?:GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)\s+\S+\s+([1-5]\d{2})\b/,
/\b(?:status|code|http)[:\s]+([1-5]\d{2})\b/i,
/\b([1-5]\d{2})\s+(?:OK|Created|Accepted|No Content|Moved|Found|Bad Request|Unauthorized|Forbidden|Not Found|Method Not Allowed|Internal Server Error|Bad Gateway|Service Unavailable|Gateway Timeout)\b/i
];
for (const pattern of patterns) {
const match = line.match(pattern);
if (match) {
const code = parseInt(match[1], 10);
if (Number.isNaN(code)) {
continue;
}
if (code >= 500) {
return { code, bucket: '5xx', match: match[1] };
}
if (code >= 400) {
return { code, bucket: '4xx', match: match[1] };
}
if (code >= 300) {
return { code, bucket: '3xx', match: match[1] };
}
if (code >= 200) {
return { code, bucket: '2xx', match: match[1] };
}
if (code >= 100) {
return { code, bucket: '1xx', match: match[1] };
}
}
}
return null;
},
async downloadLogs() {
try {
const response = await this.makeRequest('/logs', {
method: 'GET'
});
if (response && response.lines && response.lines.length > 0) {
const logsText = response.lines.join('\n');
const blob = new Blob([logsText], { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `cli-proxy-api-logs-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.log`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
this.showNotification(i18n.t('logs.download_success'), 'success');
} else {
this.showNotification(i18n.t('logs.empty_title'), 'info');
}
} catch (error) {
console.error('下载日志失败:', error);
this.showNotification(`${i18n.t('notification.download_failed')}: ${error.message}`, 'error');
}
},
async clearLogs() {
if (!confirm(i18n.t('logs.clear_confirm'))) {
return;
}
try {
const response = await this.makeRequest('/logs', {
method: 'DELETE'
});
if (response && response.status === 'ok') {
const removedCount = response.removed || 0;
const message = `${i18n.t('logs.clear_success')} (${i18n.t('logs.removed')}: ${removedCount} ${i18n.t('logs.lines')})`;
this.showNotification(message, 'success');
} else {
this.showNotification(i18n.t('logs.clear_success'), 'success');
}
this.latestLogTimestamp = null;
await this.refreshLogs(false);
} catch (error) {
console.error('清空日志失败:', error);
this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error');
}
},
toggleLogsAutoRefresh(enabled) {
if (enabled) {
if (this.logsRefreshTimer) {
clearInterval(this.logsRefreshTimer);
}
this.logsRefreshTimer = setInterval(() => {
const logsSection = document.getElementById('logs');
if (logsSection && logsSection.classList.contains('active')) {
this.refreshLogs(true);
}
}, 5000);
this.showNotification(i18n.t('logs.auto_refresh_enabled'), 'success');
} else {
if (this.logsRefreshTimer) {
clearInterval(this.logsRefreshTimer);
this.logsRefreshTimer = null;
}
this.showNotification(i18n.t('logs.auto_refresh_disabled'), 'info');
}
}
};