mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-09 23:17:21 +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>
|
<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>
|
||||||
|
|
|
@ -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;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Reference in a new issue