mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-09 15:06:37 +08:00
feat: Optimize log file highlighting (#10062)
This commit is contained in:
parent
e9c195c778
commit
a07df9d7fe
2 changed files with 83 additions and 214 deletions
|
@ -1,243 +1,113 @@
|
|||
<template>
|
||||
<span v-for="(token, index) in tokens" :key="index" :class="['token', token.type]" :style="{ color: token.color }">
|
||||
<span v-if="token.type != 'html'" class="whitespace-pre">{{ token.text }}</span>
|
||||
<span v-else v-html="token.text" class="whitespace-pre"></span>
|
||||
</span>
|
||||
<code ref="codeRef" class="log-highlight whitespace-pre" v-html="highlightedLog" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
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<{
|
||||
log: string;
|
||||
type: string;
|
||||
}>();
|
||||
|
||||
let rules = ref<TokenRule[]>([]);
|
||||
const nginxRules: TokenRule[] = [
|
||||
{
|
||||
type: 'log-level',
|
||||
pattern: /\[(error|warn|notice|info|debug)\]/gi,
|
||||
color: '#E74C3C',
|
||||
},
|
||||
{
|
||||
type: 'path',
|
||||
pattern: /(?<=[\s"])\/[^"\s]+(?:\.\w+)?(?:\?\w+=\w+)?/g,
|
||||
color: '#B87A2B',
|
||||
},
|
||||
{
|
||||
type: 'http-method',
|
||||
pattern: /(?<=)(?:GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)(?=\s)/g,
|
||||
color: '#27AE60',
|
||||
},
|
||||
{
|
||||
type: 'status-success',
|
||||
pattern: /\s(2\d{2})\s/g,
|
||||
color: '#2ECC71',
|
||||
},
|
||||
{
|
||||
type: 'status-error',
|
||||
pattern: /\s([45]\d{2})\s/g,
|
||||
color: '#E74C3C',
|
||||
},
|
||||
{
|
||||
type: 'process-info',
|
||||
pattern: /\d+#\d+/g,
|
||||
color: '#7F8C8D',
|
||||
},
|
||||
const codeRef = ref<HTMLElement | null>(null);
|
||||
const highlightedLog = ref('');
|
||||
|
||||
interface HighlightRule {
|
||||
regex: RegExp;
|
||||
className: string;
|
||||
}
|
||||
|
||||
const highlightRules: HighlightRule[] = [
|
||||
{ regex: /\b(INFO|TRACE|System|Note|notice)\b/gi, className: 'hljs-keyword' },
|
||||
{ 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' },
|
||||
{ regex: /\b\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?\b/g, className: 'hljs-number' },
|
||||
{ regex: /\b\d+:[A-Z]\b/g, className: 'hljs-symbol' },
|
||||
{ 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' },
|
||||
{ regex: /\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|ON|CREATE|DROP|ALTER)\b/gi, className: 'hljs-built_in' },
|
||||
{ regex: /\[?(Thread|PID)[-:]?\d+\]?/gi, className: 'hljs-symbol' },
|
||||
{ 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' },
|
||||
{ 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' },
|
||||
{ regex: /\b\d+(\.\d+)?(%|ms|s|GB|MB|KB)?\b/g, className: 'hljs-number' },
|
||||
{ regex: /(['])(?:\\.|[^\1\\])*?\1/g, className: 'hljs-string' },
|
||||
{ regex: /[{}\[\]()|+*%&^~!]/g, className: 'hljs-symbol' },
|
||||
];
|
||||
|
||||
const systemRules: TokenRule[] = [
|
||||
{
|
||||
type: 'log-error',
|
||||
pattern: /\[(ERROR|WARN|FATAL)\]/g,
|
||||
color: '#E74C3C',
|
||||
},
|
||||
{
|
||||
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 extraHighlightPlugin(html: string, rules: HighlightRule[] = highlightRules): string {
|
||||
let result = html;
|
||||
for (const rule of rules) {
|
||||
result = result.replace(rule.regex, `<span class="${rule.className}">$&</span>`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function hasANSICodes(text: string) {
|
||||
const ansiRegex = /\x1b\[[0-9;]*[mK]/;
|
||||
return ansiRegex.test(text);
|
||||
}
|
||||
|
||||
function tokenizeLog(log: string): Token[] {
|
||||
if (hasANSICodes(log)) {
|
||||
let html = anser.ansiToHtml(log);
|
||||
return [
|
||||
{
|
||||
text: html,
|
||||
type: 'html',
|
||||
color: '',
|
||||
},
|
||||
];
|
||||
const highlightContent = (): string => {
|
||||
if (!props.log) return '';
|
||||
if (hasANSICodes(props.log)) {
|
||||
return anser.ansiToHtml(props.log);
|
||||
} else {
|
||||
return extraHighlightPlugin(props.log);
|
||||
}
|
||||
const tokens: Token[] = [];
|
||||
let lastIndex = 0;
|
||||
let matches: { index: number; text: string; type: string; color: string }[] = [];
|
||||
};
|
||||
|
||||
rules.value.forEach((rule) => {
|
||||
const regex = new RegExp(rule.pattern.source, 'g');
|
||||
let match;
|
||||
while ((match = regex.exec(log)) !== null) {
|
||||
matches.push({
|
||||
index: match.index,
|
||||
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));
|
||||
watch(
|
||||
() => [props.log, props.type],
|
||||
() => {
|
||||
highlightedLog.value = highlightContent();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
switch (props.type) {
|
||||
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;
|
||||
}
|
||||
highlightedLog.value = highlightContent();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.token {
|
||||
font-family: 'JetBrains Mono', Monaco, Menlo, Consolas, 'Courier New', monospace;
|
||||
.log-highlight {
|
||||
color: #e06c75;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: inherit;
|
||||
white-space: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ip {
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
text-decoration-thickness: 1px;
|
||||
:deep(.hljs-attr) {
|
||||
color: #56b6c2;
|
||||
}
|
||||
: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>
|
||||
|
|
|
@ -1309,7 +1309,6 @@ const buttons = [
|
|||
openDownload(row);
|
||||
},
|
||||
disabled: (row: File.File) => {
|
||||
debugger;
|
||||
return row?.isDir;
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue