mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-01-07 23:55:15 +08:00
feat(log): Add Highlighting to Logs (#7707)
This commit is contained in:
parent
2185c3d2e0
commit
e2354b8528
8 changed files with 271 additions and 17 deletions
|
|
@ -834,7 +834,7 @@ func collectLogs(params dto.StreamLog, messageChan chan<- string, errorChan chan
|
|||
if params.Follow {
|
||||
cmdArgs = append(cmdArgs, "-f")
|
||||
}
|
||||
if params.Tail != "all" {
|
||||
if params.Tail != "0" {
|
||||
cmdArgs = append(cmdArgs, "--tail", params.Tail)
|
||||
}
|
||||
if params.Since != "all" {
|
||||
|
|
|
|||
|
|
@ -24,20 +24,26 @@
|
|||
{{ $t('commons.button.clean') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="log-container" ref="logContainer">
|
||||
<!-- <div class="log-container" ref="logContainer">
|
||||
<DynamicScroller :items="logs" :min-item-size="32" v-if="logs.length">
|
||||
<template #default="{ item, active }">
|
||||
<DynamicScrollerItem
|
||||
:item="item"
|
||||
:active="active"
|
||||
class="msgBox"
|
||||
:size-dependencies="[item]"
|
||||
:data-index="item"
|
||||
>
|
||||
<span class="log-item">{{ item }}</span>
|
||||
<DynamicScrollerItem :item="item" :active="active" :size-dependencies="[item]" :data-index="item">
|
||||
<hightlight :log="item" type="container"></hightlight>
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
</div> -->
|
||||
|
||||
<div class="log-container" ref="logContainer">
|
||||
<div class="log-spacer" :style="{ height: `${totalHeight}px` }"></div>
|
||||
<div
|
||||
v-for="(log, index) in visibleLogs"
|
||||
:key="startIndex + index"
|
||||
class="log-item"
|
||||
:style="{ top: `${(startIndex + index) * logHeight}px` }"
|
||||
>
|
||||
<hightlight :log="log" type="container"></hightlight>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -48,6 +54,7 @@ import { dateFormatForName } from '@/utils/util';
|
|||
import { onUnmounted, reactive, ref } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import hightlight from '@/components/hightlight/index.vue';
|
||||
|
||||
const props = defineProps({
|
||||
container: {
|
||||
|
|
@ -71,6 +78,15 @@ const logSearch = reactive({
|
|||
tail: 100,
|
||||
compose: '',
|
||||
});
|
||||
const logHeight = 20;
|
||||
const logCount = ref(0);
|
||||
const totalHeight = computed(() => logHeight * logCount.value);
|
||||
const startIndex = ref(0);
|
||||
const containerHeight = ref(500);
|
||||
const visibleCount = computed(() => Math.ceil(containerHeight.value / logHeight));
|
||||
const visibleLogs = computed(() => {
|
||||
return logs.value.slice(startIndex.value, startIndex.value + visibleCount.value);
|
||||
});
|
||||
|
||||
const timeOptions = ref([
|
||||
{ label: i18n.global.t('container.all'), value: 'all' },
|
||||
|
|
@ -178,6 +194,13 @@ onMounted(() => {
|
|||
logSearch.mode = 'all';
|
||||
logSearch.isWatch = true;
|
||||
|
||||
nextTick(() => {
|
||||
if (logContainer.value) {
|
||||
logContainer.value.scrollTop = totalHeight.value;
|
||||
containerHeight.value = logContainer.value.getBoundingClientRect().height;
|
||||
}
|
||||
});
|
||||
|
||||
searchLogs();
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
220
frontend/src/components/hightlight/index.vue
Normal file
220
frontend/src/components/hightlight/index.vue
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
<template>
|
||||
<span v-for="(token, index) in tokens" :key="index" :class="['token', token.type]" :style="{ color: token.color }">
|
||||
{{ token.text }}
|
||||
</span>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
interface TokenRule {
|
||||
type: string;
|
||||
pattern: RegExp;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface Token {
|
||||
text: string;
|
||||
type: string;
|
||||
color: 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:
|
||||
/(?:(?<=GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+|(?<=open\(\s*")|(?<="\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 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: '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 tokenizeLog(log: string): Token[] {
|
||||
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) {
|
||||
tokens.push({
|
||||
text: log.substring(lastIndex),
|
||||
type: 'plain',
|
||||
color: '#666666',
|
||||
});
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
const tokens = computed(() => tokenizeLog(props.log));
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.token {
|
||||
font-family: 'JetBrains Mono', Monaco, Menlo, Consolas, 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ip {
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
text-decoration-thickness: 1px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
class="log-item"
|
||||
:style="{ top: `${(startIndex + index) * logHeight}px` }"
|
||||
>
|
||||
<span>{{ log }}</span>
|
||||
<hightlight :log="log" :type="config.colorMode"></hightlight>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -36,6 +36,7 @@ import { downloadFile } from '@/utils/util';
|
|||
import { ReadByLine } from '@/api/modules/files';
|
||||
import { GlobalStore } from '@/store';
|
||||
import bus from '@/global/bus';
|
||||
import hightlight from '@/components/hightlight/index.vue';
|
||||
const globalStore = GlobalStore();
|
||||
|
||||
interface LogProps {
|
||||
|
|
@ -44,6 +45,7 @@ interface LogProps {
|
|||
name?: string;
|
||||
tail?: boolean;
|
||||
taskID?: string;
|
||||
colorMode?: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
|
|
@ -54,6 +56,7 @@ const props = defineProps({
|
|||
type: '',
|
||||
name: '',
|
||||
tail: false,
|
||||
colorMode: 'nginx',
|
||||
}),
|
||||
},
|
||||
defaultButton: {
|
||||
|
|
@ -315,7 +318,7 @@ defineExpose({ changeTail, onDownload, clearLog });
|
|||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.log-container {
|
||||
height: calc(100vh - 405px);
|
||||
height: calc(100vh - 420px);
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
|
|
@ -336,4 +339,9 @@ defineExpose({ changeTail, onDownload, clearLog });
|
|||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ const config = reactive({
|
|||
resourceID: 0,
|
||||
taskType: '',
|
||||
tail: true,
|
||||
colorMode: 'task',
|
||||
});
|
||||
const open = ref(false);
|
||||
const showTail = ref(true);
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ const hasContent = ref(false);
|
|||
const logConfig = reactive({
|
||||
type: 'system',
|
||||
name: '',
|
||||
colorMode: 'system',
|
||||
});
|
||||
const showLog = ref(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ const logConfig = reactive({
|
|||
type: 'website',
|
||||
id: undefined,
|
||||
name: 'access.log',
|
||||
colorMode: 'nginx',
|
||||
});
|
||||
const showLog = ref(false);
|
||||
const loading = ref(false);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<el-switch v-model="data.enable" @change="updateEnable"></el-switch>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<LogFile :config="{ id: id, type: 'website', name: logType }" ref="logRef">
|
||||
<LogFile :config="{ id: id, type: 'website', name: logName, colorMode: 'nginx' }" ref="logRef">
|
||||
<template #button>
|
||||
<el-button @click="cleanLog" icon="Delete">
|
||||
{{ $t('commons.button.clean') }}
|
||||
|
|
@ -32,7 +32,7 @@ const props = defineProps({
|
|||
default: 0,
|
||||
},
|
||||
});
|
||||
const logType = computed(() => {
|
||||
const logName = computed(() => {
|
||||
return props.logType;
|
||||
});
|
||||
const id = computed(() => {
|
||||
|
|
@ -52,7 +52,7 @@ const updateEnable = () => {
|
|||
const req = {
|
||||
id: id.value,
|
||||
operate: operate,
|
||||
logType: logType.value,
|
||||
logType: props.logType,
|
||||
};
|
||||
loading.value = true;
|
||||
OpWebsiteLog(req)
|
||||
|
|
@ -69,13 +69,13 @@ const clearLog = () => {
|
|||
};
|
||||
|
||||
const cleanLog = async () => {
|
||||
let log = logType.value === 'access.log' ? i18n.global.t('website.accessLog') : i18n.global.t('website.errLog');
|
||||
let log = props.logType === 'access.log' ? i18n.global.t('website.accessLog') : i18n.global.t('website.errLog');
|
||||
opRef.value.acceptParams({
|
||||
title: i18n.global.t('commons.msg.clean'),
|
||||
names: [],
|
||||
msg: i18n.global.t('commons.msg.operatorHelper', [log, i18n.global.t('commons.msg.clean')]),
|
||||
api: OpWebsiteLog,
|
||||
params: { id: id.value, operate: 'delete', logType: logType.value },
|
||||
params: { id: id.value, operate: 'delete', logType: props.logType },
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue