feat: Optimize log file highlighting (#10062)

This commit is contained in:
2025-08-19 22:47:15 +08:00 committed by GitHub
parent e9c195c778
commit a07df9d7fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 83 additions and 214 deletions

View file

@ -1,243 +1,113 @@
<template> <template>
<span v-for="(token, index) in tokens" :key="index" :class="['token', token.type]" :style="{ color: token.color }"> <code ref="codeRef" class="log-highlight whitespace-pre" v-html="highlightedLog" />
<span v-if="token.type != 'html'" class="whitespace-pre">{{ token.text }}</span>
<span v-else v-html="token.text" class="whitespace-pre"></span>
</span>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
import anser from 'anser'; import anser from 'anser';
interface TokenRule {
type: string;
pattern: RegExp;
color: string;
}
interface Token {
text: string;
type: string;
color: string;
html?: string;
}
const props = defineProps<{ const props = defineProps<{
log: string; log: string;
type: string; type: string;
}>(); }>();
let rules = ref<TokenRule[]>([]); const codeRef = ref<HTMLElement | null>(null);
const nginxRules: TokenRule[] = [ const highlightedLog = ref('');
{
type: 'log-level', interface HighlightRule {
pattern: /\[(error|warn|notice|info|debug)\]/gi, regex: RegExp;
color: '#E74C3C', className: string;
}, }
{
type: 'path', const highlightRules: HighlightRule[] = [
pattern: /(?<=[\s"])\/[^"\s]+(?:\.\w+)?(?:\?\w+=\w+)?/g, { regex: /\b(INFO|TRACE|System|Note|notice)\b/gi, className: 'hljs-keyword' },
color: '#B87A2B', { regex: /\b(ERROR|DEBUG|FATAL)\b/gi, className: 'hljs-error' },
}, { regex: /\b(WARN|WARNING)\b/gi, className: 'hljs-warn' },
{ { regex: /\b(true|false|null)\b/g, className: 'hljs-literal' },
type: 'http-method', { regex: /\b\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?\b/g, className: 'hljs-number' },
pattern: /(?<=)(?:GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)(?=\s)/g, { regex: /\b\d+:[A-Z]\b/g, className: 'hljs-symbol' },
color: '#27AE60', { regex: /\b\d+(\.\d+)?\b/g, className: 'hljs-number' },
}, { regex: /([/~]?[A-Za-z0-9._-]{1,255}(?:\/[A-Za-z0-9._-]{1,255})+)/g, className: 'hljs-string' },
{ { regex: /\b\d{1,3}(?:\.\d{1,3}){3}\b/g, className: 'hljs-attr' },
type: 'status-success', { regex: /\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|ON|CREATE|DROP|ALTER)\b/gi, className: 'hljs-built_in' },
pattern: /\s(2\d{2})\s/g, { regex: /\[?(Thread|PID)[-:]?\d+\]?/gi, className: 'hljs-symbol' },
color: '#2ECC71', { regex: /\b([A-Za-z0-9_\-./]+\.([a-z]+)):(\d+)\b/g, className: 'hljs-title' },
}, { regex: /\bhttps?:\/\/[^\s]+/gi, className: 'hljs-link' },
{ { regex: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, className: 'hljs-link' },
type: 'status-error', { regex: /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi, className: 'hljs-meta' },
pattern: /\s([45]\d{2})\s/g, { regex: /\b\d+(\.\d+)?(%|ms|s|GB|MB|KB)?\b/g, className: 'hljs-number' },
color: '#E74C3C', { regex: /(['])(?:\\.|[^\1\\])*?\1/g, className: 'hljs-string' },
}, { regex: /[{}\[\]()|+*%&^~!]/g, className: 'hljs-symbol' },
{
type: 'process-info',
pattern: /\d+#\d+/g,
color: '#7F8C8D',
},
]; ];
const systemRules: TokenRule[] = [ function extraHighlightPlugin(html: string, rules: HighlightRule[] = highlightRules): string {
{ let result = html;
type: 'log-error', for (const rule of rules) {
pattern: /\[(ERROR|WARN|FATAL)\]/g, result = result.replace(rule.regex, `<span class="${rule.className}">$&</span>`);
color: '#E74C3C', }
}, return result;
{ }
type: 'log-normal',
pattern: /\[(INFO|DEBUG)\]/g,
color: '#8B8B8B',
},
{
type: 'timestamp',
pattern: /\[\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\]/g,
color: '#8B8B8B',
},
{
type: 'bracket-text',
pattern: /\[([^\]]+)\]/g,
color: '#B87A2B',
},
{
type: 'referrer-ua',
pattern: /https?:\/\/(?:[\w-]+\.)+[\w-]+(?::\d+)?(?:\/[^\s\]\)"]*)?/g,
color: '#786C88',
},
];
const taskRules: TokenRule[] = [
{
type: 'bracket-text',
pattern: /\[(?:[^\[\]]*(?:\[[^\[\]]*\])*[^\[\]]*)*\]/g,
color: '#B87A2B',
},
];
const defaultRules: TokenRule[] = [
{
type: 'timestamp',
pattern:
/(?:\[\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2}\s[+-]\d{4}\]|\d{4}[-\/]\d{2}[-\/]\d{2}\s\d{2}:\d{2}:\d{2})/g,
color: '#8B8B8B',
},
{
type: 'referrer-ua',
pattern: /"(?:https?:\/\/[^"]+|Mozilla[^"]+|curl[^"]+)"/g,
color: '#786C88',
},
{
type: 'ip',
pattern: /\b(?<!\[)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b(?!\])/g,
color: '#4A90E2',
},
{
type: 'ipv6',
pattern: /\b(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}\b/g,
color: '#4A90E2',
},
{
type: 'server-host',
pattern: /(?:server|host):\s*[^,\s]+/g,
color: '#5D6D7E',
},
];
const containerRules: TokenRule[] = [
{
type: 'timestamp',
pattern: /\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\+\d{2}:\d{2}/g,
color: '#8B8B8B',
},
{
type: 'bracket-text',
pattern: /\[([^\]]+)\]/g,
color: '#B87A2B',
},
];
function hasANSICodes(text: string) { function hasANSICodes(text: string) {
const ansiRegex = /\x1b\[[0-9;]*[mK]/; const ansiRegex = /\x1b\[[0-9;]*[mK]/;
return ansiRegex.test(text); return ansiRegex.test(text);
} }
function tokenizeLog(log: string): Token[] { const highlightContent = (): string => {
if (hasANSICodes(log)) { if (!props.log) return '';
let html = anser.ansiToHtml(log); if (hasANSICodes(props.log)) {
return [ return anser.ansiToHtml(props.log);
{ } else {
text: html, return extraHighlightPlugin(props.log);
type: 'html',
color: '',
},
];
} }
const tokens: Token[] = []; };
let lastIndex = 0;
let matches: { index: number; text: string; type: string; color: string }[] = [];
rules.value.forEach((rule) => { watch(
const regex = new RegExp(rule.pattern.source, 'g'); () => [props.log, props.type],
let match; () => {
while ((match = regex.exec(log)) !== null) { highlightedLog.value = highlightContent();
matches.push({ },
index: match.index, { immediate: true },
text: match[0], );
type: rule.type,
color: rule.color,
});
}
});
matches.sort((a, b) => a.index - b.index);
matches = matches.filter((match, index) => {
if (index === 0) return true;
const prev = matches[index - 1];
return match.index >= prev.index + prev.text.length;
});
matches.forEach((match) => {
if (match.index > lastIndex) {
tokens.push({
text: log.substring(lastIndex, match.index),
type: 'plain',
color: '#666666',
});
}
tokens.push({
text: match.text,
type: match.type,
color: match.color,
});
lastIndex = match.index + match.text.length;
});
if (lastIndex < log.length) {
const rest = log.substring(lastIndex).replace(/\n?$/, '\n');
tokens.push({
text: rest,
type: 'plain',
color: '#666666',
});
}
return tokens;
}
const tokens = computed(() => tokenizeLog(props.log));
onMounted(() => { onMounted(() => {
switch (props.type) { highlightedLog.value = highlightContent();
case 'nginx':
rules.value = nginxRules.concat(defaultRules);
break;
case 'system':
rules.value = systemRules.concat(defaultRules);
break;
case 'container':
rules.value = containerRules.concat(defaultRules);
break;
case 'task':
rules.value = taskRules.concat(defaultRules);
break;
default:
rules.value = defaultRules;
break;
}
}); });
</script> </script>
<style scoped> <style scoped>
.token { .log-highlight {
font-family: 'JetBrains Mono', Monaco, Menlo, Consolas, 'Courier New', monospace; color: #e06c75;
font-size: 14px; font-size: 14px;
font-weight: 500; line-height: inherit;
white-space: inherit;
text-align: left;
} }
.ip { :deep(.hljs-attr) {
text-decoration: underline; color: #56b6c2;
text-decoration-style: dotted; }
text-decoration-thickness: 1px; :deep(.hljs-built_in, .hljs-meta) {
color: #e6c07b;
}
:deep(.hljs-title) {
color: #d19a66;
}
:deep(.hljs-symbol) {
color: #61afef;
}
:deep(.hljs-link) {
color: #56b6c2;
}
:deep(.hljs-debug) {
color: #61aeee !important;
}
:deep(.hljs-info) {
color: #00bb00 !important;
}
:deep(.hljs-warn) {
color: #bbbb00 !important;
}
:deep(.hljs-error) {
color: #f91306 !important;
font-weight: bold;
} }
</style> </style>

View file

@ -1309,7 +1309,6 @@ const buttons = [
openDownload(row); openDownload(row);
}, },
disabled: (row: File.File) => { disabled: (row: File.File) => {
debugger;
return row?.isDir; return row?.isDir;
}, },
}, },