优化日志,错误标记

This commit is contained in:
marvzhang
2020-02-13 11:15:51 +08:00
parent 0ea95e676a
commit 7c4ddd2824
9 changed files with 298 additions and 85 deletions

View File

@@ -11,6 +11,15 @@
</el-form-item>
<el-form-item :label="$t('Status')">
<status-tag :status="taskForm.status"/>
<el-badge
v-if="errorLogData.length > 0"
:value="errorLogData.length"
style="margin-left:10px; cursor:pointer;"
>
<el-tag type="danger" @click="onClickLogWithErrors">
{{$t('Log with errors')}}
</el-tag>
</el-badge>
</el-form-item>
<el-form-item :label="$t('Log File Path')">
<el-input v-model="taskForm.log_path" placeholder="Log File Path" disabled></el-input>
@@ -28,7 +37,7 @@
<el-input :value="getTime(taskForm.finish_ts)" placeholder="Finish Time" disabled></el-input>
</el-form-item>
<el-form-item :label="$t('Wait Duration (sec)')">
<el-input :value="getWaitDuration(taskForm)" placeholder="Wait Duration" disabled></el-input>
<el-input :value="getWaitDuration(taskForm)" placeholder="Wait Duration" disabled></el-input>
</el-form-item>
<el-form-item :label="$t('Runtime Duration (sec)')">
<el-input :value="getRuntimeDuration(taskForm)" placeholder="Runtime Duration" disabled></el-input>
@@ -37,7 +46,7 @@
<el-input :value="getTotalDuration(taskForm)" placeholder="Runtime Duration" disabled></el-input>
</el-form-item>
<el-form-item :label="$t('Results Count')">
<el-input v-model="taskForm.result_count" placeholder="Results Count" disabled></el-input>
<el-input v-model="taskForm.result_count" placeholder="Results Count" disabled></el-input>
</el-form-item>
<!--<el-form-item :label="$t('Average Results Count per Second')">-->
<!--<el-input v-model="taskForm.avg_num_results" placeholder="Average Results Count per Second" disabled>-->
@@ -59,7 +68,8 @@
<script>
import {
mapState
mapState,
mapGetters
} from 'vuex'
import StatusTag from '../Status/StatusTag'
import dayjs from 'dayjs'
@@ -69,7 +79,11 @@ export default {
components: { StatusTag },
computed: {
...mapState('task', [
'taskForm'
'taskForm',
'taskLog'
]),
...mapGetters('task', [
'errorLogData'
]),
isRunning () {
return ['pending', 'running'].includes(this.taskForm.status)
@@ -99,6 +113,9 @@ export default {
getTotalDuration (row) {
if (!row.finish_ts || row.finish_ts.match('^0001')) return 'NA'
return dayjs(row.finish_ts).diff(row.create_ts, 'second')
},
onClickLogWithErrors () {
this.$emit('click-log')
}
}
}

View File

@@ -3,7 +3,7 @@
<el-col :span="12" style="padding-right: 20px;">
<el-row class="task-info-overview-wrapper wrapper">
<h4 class="title">{{$t('Task Info')}}</h4>
<task-info-view/>
<task-info-view @click-log="() => $emit('click-log')"/>
</el-row>
<el-row style="border-bottom:1px solid #e4e7ed;margin:0 0 20px 0;padding-bottom:20px;"/>
</el-col>

View File

@@ -1,9 +1,9 @@
<template>
<div class="log-item" :style="style">
<div class="log-item" :style="style" :class="`log-item-${index} ${active ? 'active' : ''}`">
<div class="line-no">{{index}}</div>
<div class="line-content">
<span v-if="isLogEnd" style="color: #E6A23C" class="loading-text">
{{$t('Updating log...')}}
<span v-if="isLogEnd" style="color: #E6A23C">
<span class="loading-text">{{$t('Updating log...')}}</span>
<i class="el-icon-loading"></i>
</span>
<span v-else-if="isAnsi" v-html="dataHtml"></span>
@@ -31,13 +31,15 @@ export default {
searchString: {
type: String,
default: ''
},
active: {
type: Boolean,
default: false
}
},
data () {
const token = ' :,.'
return {
errorRegex: new RegExp(`(?:[${token}]|^)((?:error|exception|traceback)s?)(?:[${token}]|$)`, 'gi')
// errorRegex: new RegExp('(error|exception|traceback)', 'gi')
errorRegex: this.$utils.log.errorRegex
}
},
computed: {
@@ -65,42 +67,56 @@ export default {
<style scoped>
.log-item {
display: table;
display: block;
}
.log-item:hover {
background: rgba(55, 57, 59, 0.5);
}
.log-item:first-child .line-no {
padding-top: 10px;
text-align: right;
}
.log-item .line-no {
display: table-cell;
display: inline-block;
width: 70px;
color: #A9B7C6;
background: #313335;
padding-right: 10px;
text-align: right;
flex-basis: 40px;
width: 70px;
}
.log-item.active .line-no {
background: #E6A23C;
color: white;
font-weight: bolder;
}
.log-item .line-content {
padding-left: 10px;
display: table-cell;
/*display: inline-block;*/
word-break: break-word;
flex-basis: calc(100% - 50px);
display: inline-block;
width: calc(100% - 70px);
white-space: nowrap;
}
.loading-text {
animation: blink;
margin-right: 5px;
animation: blink 2s ease-in infinite;
}
@keyframes blink {
0% {
opacity: 0;
opacity: 1;
}
50% {
opacity: 0.3;
}
100% {
opacity: 100%;
opacity: 1;
}
}
</style>

View File

@@ -1,43 +1,90 @@
<template>
<div class="log-view-wrapper">
<div class="log-view-container">
<div class="filter-wrapper">
<el-button
size="small"
type="primary"
icon="el-icon-download"
style="margin-right: 10px"
:disabled="isToBottom"
@click="onAutoScroll"
>
{{$t('Auto-Scroll')}}
</el-button>
<el-input
v-model="searchString"
size="small"
suffix-icon="el-icon-search"
:placeholder="$t('Search Log')"
style="width: 240px"
/>
<div class="left">
<el-button
size="small"
type="primary"
icon="el-icon-download"
style="margin-right: 10px"
:disabled="isToBottom"
@click="onAutoScroll"
>
{{$t('Auto-Scroll')}}
</el-button>
<el-input
v-model="searchString"
size="small"
suffix-icon="el-icon-search"
:placeholder="$t('Search Log')"
style="width: 240px; margin-right: 10px"
/>
</div>
<div class="right">
<el-badge
v-if="errorLogData.length > 0"
:value="errorLogData.length"
>
<el-button
type="danger"
size="small"
icon="el-icon-warning-outline"
@click="toggleErrors"
>
{{$t('Error Count')}}
</el-button>
</el-badge>
</div>
</div>
<div class="log-view-wrapper" ref="log-view-wrapper">
<virtual-list
class="log-view"
ref="log-view"
:size="6"
:remain="100"
:item="item"
:itemcount="filteredLogData.length"
:itemprops="getItemProps"
:tobottom="onToBottom"
:onscroll="onScroll"
/>
<div class="content">
<div
class="log-view-wrapper"
:class="isErrorsCollapsed ? 'errors-collapsed' : ''"
>
<virtual-list
class="log-view"
ref="log-view"
:start="currentLogIndex - 1"
:offset="0"
:size="18"
:remain="remainSize"
:item="item"
:itemcount="filteredLogData.length"
:itemprops="getItemProps"
:tobottom="onToBottom"
:onscroll="onScroll"
/>
</div>
<div
v-show="!isErrorsCollapsed && !isErrorCollapsing"
class="errors-wrapper"
:class="isErrorsCollapsed ? 'collapsed' : ''"
>
<ul class="error-list">
<li
v-for="item in errorLogData"
:key="item.index"
class="error-item"
:class="currentLogIndex === item.index ? 'active' : ''"
@click="onClickError(item)"
>
<span class="line-no">
{{item.index}}
</span>
<span class="line-content">
{{item.data}}
</span>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import {
mapState
mapState,
mapGetters
} from 'vuex'
import VirtualList from 'vue-virtual-scroll-list'
import Convert from 'ansi-to-html'
@@ -63,38 +110,38 @@ export default {
searchString: '',
isToBottom: false,
isScrolling: false,
isScrolling2nd: false
isScrolling2nd: false,
errorRegex: this.$utils.log.errorRegex,
currentOffset: 0,
isErrorsCollapsed: true,
isErrorCollapsing: false
}
},
computed: {
...mapState('task', [
'taskForm'
]),
logData () {
const data = this.data.split('\n')
.map((d, i) => {
return {
index: i + 1,
data: d
}
})
if (this.taskForm && this.taskForm.status === 'running') {
data.push({
index: data.length + 1,
data: '###LOG_END###'
})
data.push({
index: data.length + 2,
data: ''
})
...mapGetters('task', [
'logData',
'errorLogData'
]),
currentLogIndex: {
get () {
return this.$store.state.task.currentLogIndex
},
set (value) {
this.$store.commit('task/SET_CURRENT_LOG_INDEX', value)
}
return data
},
filteredLogData () {
return this.logData.filter(d => {
if (!this.searchString) return true
return !!d.data.toLowerCase().match(this.searchString.toLowerCase())
})
},
remainSize () {
const height = document.querySelector('body').clientHeight
return (height - 240) / 18
}
},
watch: {
@@ -113,6 +160,7 @@ export default {
index: logItem.index,
data: isAnsi ? convert.toHtml(logItem.data) : logItem.data,
searchString: this.searchString,
active: logItem.active,
isAnsi
}
}
@@ -144,6 +192,23 @@ export default {
},
onAutoScroll () {
this.toBottom()
},
toggleErrors () {
this.isErrorsCollapsed = !this.isErrorsCollapsed
this.isErrorCollapsing = true
setTimeout(() => {
this.isErrorCollapsing = false
}, 300)
},
onClickError (item) {
this.currentLogIndex = item.index
this.isToBottom = false
const handle = setInterval(() => {
this.isToBottom = false
}, 10)
setTimeout(() => {
clearInterval(handle)
}, 500)
}
},
mounted () {
@@ -151,7 +216,7 @@ export default {
if (this.isToBottom) {
this.toBottom()
}
}, 100)
}, 500)
},
destroyed () {
clearInterval(this.handle)
@@ -160,15 +225,94 @@ export default {
</script>
<style scoped>
.filter-wrapper {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.content {
display: block;
}
.log-view-wrapper {
float: left;
flex-basis: calc(100% - 240px);
width: calc(100% - 300px);
transition: width 0.3s;
}
.log-view-wrapper.errors-collapsed {
flex-basis: 100%;
width: 100%;
}
.log-view {
margin-top: 0 !important;
overflow-y: scroll !important;
list-style: none;
color: #A9B7C6;
background: #2B2B2B;
border: none;
}
.filter-wrapper {
margin-bottom: 10px;
.errors-wrapper {
float: left;
display: inline-block;
margin: 0;
padding: 0;
flex-basis: 240px;
width: 300px;
transition: opacity 0.3s;
border-top: 1px solid #DCDFE6;
border-right: 1px solid #DCDFE6;
border-bottom: 1px solid #DCDFE6;
height: calc(100vh - 240px);
font-size: 16px;
}
.errors-wrapper.collapsed {
width: 0;
}
.errors-wrapper .error-list {
list-style: none;
padding: 0;
margin: 0;
}
.errors-wrapper .error-list .error-item {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
/*height: 18px;*/
border-bottom: 1px solid white;
padding: 5px 0;
background: #F56C6C;
color: white;
cursor: pointer;
}
.errors-wrapper .error-list .error-item.active {
background: #E6A23C;
font-weight: bolder;
text-decoration: underline;
}
.errors-wrapper .error-list .error-item:hover {
font-weight: bolder;
text-decoration: underline;
}
.errors-wrapper .error-list .error-item .line-no {
display: inline-block;
text-align: right;
width: 70px;
}
.errors-wrapper .error-list .error-item .line-content {
display: inline;
width: calc(100% - 70px);
padding-left: 10px;
}
</style>

View File

@@ -211,6 +211,8 @@ export default {
'Search Log': '搜索日志',
'Auto-Scroll': '自动滚动',
'Updating log...': '正在更新日志...',
'Error Count': '错误数',
'Log with errors': '日志错误',
// 任务列表
'Node': '节点',

View File

@@ -1,4 +1,5 @@
import request from '../../api/request'
import utils from '../../utils'
const state = {
// TaskList
@@ -6,6 +7,7 @@ const state = {
taskListTotalCount: 0,
taskForm: {},
taskLog: '',
currentLogIndex: 0,
taskResultsData: [],
taskResultsColumns: [],
taskResultsTotalCount: 0,
@@ -36,6 +38,32 @@ const getters = {
}
}
return keys
},
logData (state) {
const data = state.taskLog.split('\n')
.map((d, i) => {
return {
index: i + 1,
data: d,
active: state.currentLogIndex === i + 1
}
})
if (state.taskForm && state.taskForm.status === 'running') {
data.push({
index: data.length + 1,
data: '###LOG_END###'
})
data.push({
index: data.length + 1,
data: ''
})
}
return data
},
errorLogData (state, getters) {
return getters.logData.filter(d => {
return d.data.match(utils.log.errorRegex)
})
}
}
@@ -49,6 +77,9 @@ const mutations = {
SET_TASK_LOG (state, value) {
state.taskLog = value
},
SET_CURRENT_LOG_INDEX (state, value) {
state.currentLogIndex = value
},
SET_TASK_RESULTS_DATA (state, value) {
state.taskResultsData = value
},

View File

@@ -1,9 +1,11 @@
import stats from './stats'
import encrypt from './encrypt'
import tour from './tour'
import log from './log'
export default {
stats,
encrypt,
tour
tour,
log
}

View File

@@ -0,0 +1,5 @@
const regexToken = ' :,.'
export default {
errorRegex: new RegExp(`(?:[${regexToken}]|^)((?:error|exception|traceback)s?)(?:[${regexToken}]|$)`, 'gi')
}

View File

@@ -12,10 +12,10 @@
<!--tabs-->
<el-tabs v-model="activeTabName" @tab-click="onTabClick" type="border-card">
<el-tab-pane :label="$t('Overview')" name="overview">
<task-overview/>
<task-overview @click-log="activeTabName = 'log'"/>
</el-tab-pane>
<el-tab-pane :label="$t('Log')" name="log">
<log-view :data="taskLog"/>
<log-view/>
</el-tab-pane>
<el-tab-pane :label="$t('Results')" name="results">
<div class="button-group">
@@ -55,7 +55,6 @@ export default {
return {
activeTabName: 'overview',
handle: undefined,
taskLog: '',
// tutorial
tourSteps: [
@@ -137,7 +136,8 @@ export default {
...mapState('task', [
'taskForm',
'taskResultsData',
'taskResultsTotalCount'
'taskResultsTotalCount',
'taskLog'
]),
...mapGetters('task', [
'taskResultsColumns'
@@ -186,11 +186,7 @@ export default {
this.$st.sendEv('任务详情', '结果', '下载CSV')
},
getTaskLog () {
if (this.$route.params.id) {
request.get(`/tasks/${this.$route.params.id}/log`).then(response => {
this.taskLog = response.data.data
})
}
this.$store.dispatch('task/getTaskLog', this.$route.params.id)
}
},
created () {