feat: 任务批次新增日志详情

This commit is contained in:
xlsea 2024-05-07 14:51:19 +08:00
parent 790e4a71be
commit 713c643544
10 changed files with 426 additions and 76 deletions

View File

@ -0,0 +1,101 @@
<script setup lang="ts">
import { computed, nextTick, onUnmounted, reactive, ref } from 'vue';
import { useAppStore } from '@/store/modules/app';
defineOptions({
name: 'DetailDrawer'
});
interface Props {
title?: string;
width?: [string, string];
}
const props = defineProps<Props>();
interface Emits {
(e: 'update:modelValue', modelValue: boolean): void;
}
const emit = defineEmits<Emits>();
const model = defineModel<boolean>({ default: false });
const slots = defineSlots();
const appStore = useAppStore();
const state = reactive({ width: 0 });
const isFullscreen = ref(false);
const drawerWidth = computed(() => {
if (props.width) {
return isFullscreen.value ? props.width[1] : props.width[0];
}
const maxMinWidth = 360;
const maxMaxWidth = 600;
if (appStore.isMobile) {
return state.width * 0.9 >= maxMinWidth ? `${maxMinWidth}px` : '90%';
}
let minWidth = state.width * 0.3 >= maxMinWidth ? `${maxMinWidth}px` : '30%';
minWidth = state.width <= 420 ? '90%' : minWidth;
let maxWidth = state.width * 0.5 >= maxMaxWidth ? `${maxMaxWidth}px` : '50%';
maxWidth = state.width <= 420 ? '90%' : maxWidth;
return isFullscreen.value ? maxWidth : minWidth;
});
const getState = () => {
state.width = document.documentElement.clientWidth;
};
nextTick(() => {
getState();
window.addEventListener('resize', getState);
});
onUnmounted(() => {
//
window.removeEventListener('resize', getState);
});
const onUpdateShow = (value: boolean) => {
emit('update:modelValue', value);
};
</script>
<template>
<NDrawer v-model:show="model" display-directive="if" :width="drawerWidth" @update:show="onUpdateShow">
<NDrawerContent :title="props.title" :native-scrollbar="false" closable header-class="operate-dawer-header">
<template #header>
{{ props.title }}
<div
v-if="!appStore.isMobile && (!props.width || (props.width && props.width[0] !== props.width[1]))"
quaternary
class="fullscreen text-18px color-#6a6a6a"
@click="isFullscreen = !isFullscreen"
>
<icon-material-symbols:close-fullscreen-rounded v-if="isFullscreen" />
<icon-material-symbols:open-in-full-rounded v-else />
</div>
</template>
<slot></slot>
<template v-if="slots.footer" #footer>
<slot name="footer"></slot>
</template>
</NDrawerContent>
</NDrawer>
</template>
<style scoped>
.fullscreen {
height: 22px;
width: 22px;
display: flex;
justify-content: center;
align-items: center;
}
.fullscreen:hover {
background-color: #e8e8e8;
color: #696969;
border-radius: 6px;
cursor: pointer;
}
</style>

View File

@ -1,6 +1,7 @@
<script setup lang="tsx">
import { NCollapse, NCollapseItem } from 'naive-ui';
import { defineComponent, ref, watch } from 'vue';
import { defineComponent, watch } from 'vue';
import { $t } from '@/locales';
defineOptions({
name: 'LogDrawer'
@ -9,11 +10,13 @@ defineOptions({
interface Props {
title?: string;
show?: boolean;
modelValue?: Api.JobLog.JobMessage[];
}
const props = withDefaults(defineProps<Props>(), {
title: '日志详情',
show: false
title: $t('page.log.title'),
show: false,
modelValue: () => []
});
interface Emits {
@ -25,38 +28,6 @@ const visible = defineModel<boolean>('visible', {
default: true
});
const messageList = ref([
{
time_stamp: '1712021845601',
level: 'ERROR',
port: '8018',
throwable:
'java.lang.ArithmeticException: / by zero\n\tat com.example.easy.retry.service.impl.RemoteRetryServiceImpl.remoteSync(RemoteRetryServiceImpl.java:46)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)\n\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n\tat org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)\n\tat org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:756)\n\tat com.aizuda.easy.retry.client.core.intercepter.EasyRetryInterceptor.invoke(EasyRetryInterceptor.java:92)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:756)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708)\n\tat com.example.easy.retry.service.impl.RemoteRetryServiceImpl$$SpringCGLIB$$0.remoteSync(<generated>)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)\n\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:568)\n\tat org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:281)\n\tat com.aizuda.easy.retry.client.core.strategy.ExecutorAnnotationMethod.doExecute(ExecutorAnnotationMethod.java:28)\n\tat com.aizuda.easy.retry.client.core.executor.AbstractRetryExecutor.doExecute(AbstractRetryExecutor.java:32)\n\tat com.aizuda.easy.retry.client.core.executor.AbstractRetryExecutor.execute(AbstractRetryExecutor.java:23)\n\tat com.aizuda.easy.retry.client.core.strategy.RemoteRetryStrategies.lambda$doGetCallable$2(RemoteRetryStrategies.java:89)\n\tat com.github.rholder.retry.AttemptTimeLimiters$NoAttemptTimeLimit.call(AttemptTimeLimiters.java:78)\n\tat com.github.rholder.retry.Retryer.call(Retryer.java:160)\n\tat com.aizuda.easy.retry.client.core.executor.GuavaRetryExecutor.call(GuavaRetryExecutor.java:56)\n\tat com.aizuda.easy.retry.client.core.strategy.AbstractRetryStrategies.openRetry(AbstractRetryStrategies.java:88)\n\tat com.aizuda.easy.retry.client.core.client.RetryEndPoint.dispatch(RetryEndPoint.java:99)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)',
host: '127.0.0.1',
location: 'com.aizuda.easy.retry.client.core.client.RetryEndPoint.dispatch(RetryEndPoint.java:124)',
thread: 'http-nio-8018-exec-5',
message: 'remote retry complete. count:[1] '
},
{
time_stamp: '1712021875512',
level: 'INFO',
port: '8018',
host: '127.0.0.1',
location: 'com.aizuda.easy.retry.client.core.client.RetryEndPoint.dispatch(RetryEndPoint.java:117)',
thread: 'http-nio-8018-exec-2',
message: 'remote retry complete. count:[4] result:[null]'
},
{
time_stamp: '1712021875512',
level: 'ERROR',
port: '8018',
host: '127.0.0.1',
location: 'com.aizuda.easy.retry.client.core.client.RetryEndPoint.dispatch(RetryEndPoint.java:124)',
thread: 'http-nio-8018-exec-2',
message: 'remote retry complete. count:[4] '
}
]);
const ThrowableComponent = defineComponent({
props: {
throwable: String
@ -109,12 +80,12 @@ function timestampToDate(timestamp: string): string {
<template>
<NDrawer v-model:show="visible" width="100%" display-directive="if" @update:show="onUpdateShow">
<NDrawerContent title="日志详情" closable>
<NDrawerContent :title="title" closable>
<div class="snail-log">
<div class="snail-log-scrollbar">
<code>
<pre
v-for="(message, index) in messageList"
v-for="(message, index) in modelValue"
:key="index"
><NDivider v-if="index !== 0" /><span class="log-hljs-time">{{timestampToDate(message.time_stamp)}}</span><span :class="`log-hljs-level-${message.level}`">{{`\t${message.level}\t`}}</span><span class="log-hljs-thread">{{`[${message.thread}]\t`}}</span><span class="log-hljs-location">{{`${message.location}: \n`}}</span> -<span class="pl-6px">{{`${message.message}\n`}}</span><ThrowableComponent :throwable="message.throwable" /></pre>
</code>

View File

@ -976,7 +976,17 @@ const local: App.I18n.Schema = {
jobName: 'Please enter job name',
taskBatchStatus: 'Please enter state'
},
detail: 'Job Batch Detail'
detail: 'Job Batch Detail',
jobTask: {
title: 'JobTask 列表',
id: 'ID',
groupName: '组名称',
clientInfo: '地址',
argsStr: '参数',
resultMessage: '结果',
retryCount: '重试次数',
createDt: '开始执行时间'
}
},
userManager: {
title: 'UserCenter List',
@ -1001,6 +1011,11 @@ const local: App.I18n.Schema = {
user: 'User',
admin: 'Admin'
}
},
log: {
title: 'Log Detail',
view: 'View Log',
info: 'Info'
}
},
form: {

View File

@ -971,7 +971,17 @@ const local: App.I18n.Schema = {
jobName: '请输入任务名称',
taskBatchStatus: '请输入状态'
},
detail: '执行批次详情'
detail: '执行批次详情',
jobTask: {
title: 'JobTask 列表',
id: 'ID',
groupName: '组名称',
clientInfo: '地址',
argsStr: '参数',
resultMessage: '结果',
retryCount: '重试次数',
createDt: '开始执行时间'
}
},
userManager: {
title: '用户列表',
@ -996,6 +1006,11 @@ const local: App.I18n.Schema = {
user: '普通用户',
admin: '管理员'
}
},
log: {
title: '日志详情',
view: '查看日志',
info: '基本信息'
}
},
form: {

View File

@ -18,6 +18,15 @@ export function fetchGetJobList(params?: Api.Job.JobSearchParams) {
});
}
/** get Job Task list */
export function fetchGetJobTaskList(params?: Api.Job.jobTaskSearchParams) {
return request<Api.Job.JobTaskList>({
url: '/job/task/list',
method: 'get',
params
});
}
/** add Job */
export function fetchAddJob(data: Api.Job.Job) {
return request<boolean>({

19
src/service/api/log.ts Normal file
View File

@ -0,0 +1,19 @@
import { request } from '../request';
/** get Job Log List */
export function fetchJobLogList(params?: Api.JobLog.JobLogSearchParams) {
return request<Api.JobLog.JobLogList>({
url: '/job/log/list',
method: 'get',
params
});
}
/** get Retry Log List */
export function fetchRetryLogList(params?: Api.JobLog.JobLogSearchParams) {
return request<Api.JobLog.JobLogList>({
url: '/retry-task-log/message/list',
method: 'get',
params
});
}

64
src/typings/api.d.ts vendored
View File

@ -986,6 +986,36 @@ declare namespace Api {
jobId?: number;
keywords?: string;
}>;
/** jobTask */
type JobTask = Common.CommonRecord<{
/** ID */
id: string;
/** 任务 ID */
jobId: string;
/** 组名称 */
groupName: string;
/** 地址 */
clientInfo: string;
/** 参数 */
argsStr: string;
/** 结果 */
resultMessage: string;
/** 重试次数 */
retryCount: string;
/** 开始执行时间 */
createDt: string;
/** 任务批次 ID */
taskBatchId: string;
}>;
/** jobTask search params */
type jobTaskSearchParams = CommonType.RecordNullable<
Pick<Api.Job.JobTask, 'groupName' | 'taskBatchId'> & CommonSearchParams & { startId: number; fromIndex: number }
>;
/** jobTask list */
type JobTaskList = Common.PaginatingQueryRecord<JobTask>;
}
/**
@ -1159,4 +1189,38 @@ declare namespace Api {
/** 1、user 2、admin */
type Role = 1 | 2;
}
/**
* namespace JobLog
*
* backend api module: "JobLog"
*/
namespace JobLog {
type JobLevel = 'INFO' | 'WARN' | 'ERROR' | 'DEBUG';
type JobLogSearchParams = {
taskBatchId: string;
jobId: string;
taskId: string;
startId: string;
fromIndex: number;
size: number;
};
type JobLogList = {
finished: boolean;
fromIndex: number;
message: JobMessage[];
nextStartId: string;
};
type JobMessage = {
level: JobLevel;
location: string;
message: string;
thread: string;
['time_stamp']: string;
throwable: string;
};
}
}

15
src/typings/app.d.ts vendored
View File

@ -1119,6 +1119,16 @@ declare namespace App {
taskBatchStatus: string;
};
detail: string;
jobTask: {
title: string;
id: string;
groupName: string;
clientInfo: string;
argsStr: string;
resultMessage: string;
retryCount: string;
createDt: string;
};
};
userManager: {
title: string;
@ -1144,6 +1154,11 @@ declare namespace App {
admin: string;
};
};
log: {
title: string;
view: string;
info: string;
};
};
form: {
required: string;

View File

@ -151,7 +151,7 @@ watch(
class="sm:h-full"
/>
</NCard>
<JobBatchDetailDrawer v-model:visible="detailVisible" :row-data="detailData" />
<JobBatchDetailDrawer v-if="detailVisible" v-model:visible="detailVisible" :row-data="detailData" />
</div>
</template>

View File

@ -1,9 +1,13 @@
<script setup lang="ts">
import { watch } from 'vue';
<script setup lang="tsx">
import { NButton } from 'naive-ui';
import { onBeforeUnmount, ref } from 'vue';
import { executorTypeRecord, operationReasonRecord, taskBatchStatusRecord } from '@/constants/business';
import { $t } from '@/locales';
// import { fetchGetJobBatchDetail } from '@/service/api';
import { tagColor } from '@/utils/common';
import { useTable } from '@/hooks/common/table';
import { fetchGetJobTaskList } from '@/service/api';
import { fetchJobLogList } from '@/service/api/log';
defineOptions({
name: 'JobBatchDetailDrawer'
@ -13,50 +17,187 @@ interface Props {
/** row data */
rowData?: Api.JobBatch.JobBatch | null;
}
const props = defineProps<Props>();
const taskData = ref<Api.Job.JobTask>();
const visible = defineModel<boolean>('visible', {
default: false
});
const logShow = defineModel<boolean>('logShow', {
default: false
});
watch(
() => visible.value,
async val => {
if (val === true) {
console.log(props.rowData?.id);
}
const { columns, data, loading, mobilePagination } = useTable({
apiFn: fetchGetJobTaskList,
apiParams: {
page: 1,
size: 10,
groupName: props.rowData?.groupName,
taskBatchId: props.rowData?.id,
startId: 0,
fromIndex: 0
// if you want to use the searchParams in Form, you need to define the following properties, and the value is null
// the value can not be undefined, otherwise the property in Form will not be reactive
},
{ immediate: true }
);
columns: () => [
{
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64,
render: row => {
async function openLog() {
logShow.value = true;
taskData.value = row;
await getLogList();
}
return (
<NButton type="info" text onClick={openLog}>
<span class="w-28px ws-break-spaces">{$t('page.log.view')}</span>
</NButton>
);
}
},
{
key: 'id',
title: $t('page.jobBatch.jobTask.id'),
align: 'left',
minWidth: 64
},
{
key: 'groupName',
title: $t('page.jobBatch.jobTask.groupName'),
align: 'left',
minWidth: 120
},
{
key: 'clientInfo',
title: $t('page.jobBatch.jobTask.clientInfo'),
align: 'left',
minWidth: 120,
render: row => {
if (row.clientInfo) {
const parts = row.clientInfo?.split('@');
const result = parts.length > 1 ? parts[1] : '';
return <div>{result}</div>;
}
return <div>{row.clientInfo}</div>;
}
},
{
key: 'argsStr',
title: $t('page.jobBatch.jobTask.argsStr'),
align: 'left',
minWidth: 120
},
{
key: 'resultMessage',
title: $t('page.jobBatch.jobTask.resultMessage'),
align: 'left',
minWidth: 120
},
{
key: 'retryCount',
title: $t('page.jobBatch.jobTask.retryCount'),
align: 'left',
minWidth: 64
},
{
key: 'createDt',
title: $t('page.jobBatch.jobTask.createDt'),
align: 'left',
minWidth: 120
}
]
});
const logList = ref<Api.JobLog.JobMessage[]>([]);
const interval = ref<NodeJS.Timeout>();
const controller = new AbortController();
const finished = ref<boolean>(false);
let startId = '0';
let fromIndex: number = 0;
async function getLogList() {
const { data: logData, error } = await fetchJobLogList({
taskBatchId: taskData.value!.taskBatchId,
jobId: taskData.value!.jobId,
taskId: taskData.value!.id,
startId,
fromIndex,
size: 50
});
if (!error) {
finished.value = logData.finished;
startId = logData.nextStartId;
fromIndex = logData.fromIndex;
if (logData.message) {
logList.value.push(...logData.message);
logList.value.sort((a, b) => Number.parseInt(a.time_stamp, 10) - Number.parseInt(b.time_stamp, 10));
}
if (!finished.value) {
clearTimeout(interval.value);
interval.value = setTimeout(getLogList, 1000);
}
}
}
const stopLog = () => {
finished.value = true;
controller.abort();
clearTimeout(interval.value);
interval.value = undefined;
};
onBeforeUnmount(() => {
stopLog();
});
</script>
<template>
<OperateDrawer v-model="visible" :title="$t('page.jobBatch.detail')">
<NDescriptions label-placement="top" bordered :column="2">
<NDescriptionsItem :label="$t('page.jobBatch.groupName')">{{ rowData?.groupName }}</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.jobBatch.jobName')">{{ rowData?.jobName }}</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.jobBatch.taskBatchStatus')">
<NTag :type="tagColor(rowData?.taskBatchStatus!)">
{{ $t(taskBatchStatusRecord[rowData?.taskBatchStatus!]) }}
</NTag>
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.jobBatch.executionAt')">{{ rowData?.executionAt }}</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.jobBatch.operationReason')">
<NTag :type="tagColor(rowData?.operationReason!)">
{{ $t(operationReasonRecord[rowData?.operationReason!]) }}
</NTag>
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.jobBatch.executorType')">
<NTag :type="tagColor(rowData?.executorType!)">
{{ $t(executorTypeRecord[rowData?.executorType!]) }}
</NTag>
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.jobBatch.executorInfo')" :span="2">
{{ rowData?.executorInfo }}
</NDescriptionsItem>
<NDescriptionsItem :label="$t('common.createDt')" :span="2">{{ rowData?.createDt }}</NDescriptionsItem>
</NDescriptions>
</OperateDrawer>
<DetailDrawer v-model="visible" :title="$t('page.jobBatch.detail')" :width="['50%', '90%']">
<NTabs type="segment" animated>
<NTabPane :name="0" :tab="$t('page.log.info')">
<NDescriptions label-placement="top" bordered :column="2">
<NDescriptionsItem :label="$t('page.jobBatch.groupName')">{{ rowData?.groupName }}</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.jobBatch.jobName')">{{ rowData?.jobName }}</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.jobBatch.taskBatchStatus')">
<NTag :type="tagColor(rowData?.taskBatchStatus!)">
{{ $t(taskBatchStatusRecord[rowData?.taskBatchStatus!]) }}
</NTag>
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.jobBatch.executionAt')">{{ rowData?.executionAt }}</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.jobBatch.operationReason')">
<NTag :type="tagColor(rowData?.operationReason!)">
{{ $t(operationReasonRecord[rowData?.operationReason!]) }}
</NTag>
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.jobBatch.executorType')">
<NTag :type="tagColor(rowData?.executorType!)">
{{ $t(executorTypeRecord[rowData?.executorType!]) }}
</NTag>
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.jobBatch.executorInfo')" :span="2">
{{ rowData?.executorInfo }}
</NDescriptionsItem>
<NDescriptionsItem :label="$t('common.createDt')" :span="2">{{ rowData?.createDt }}</NDescriptionsItem>
</NDescriptions>
</NTabPane>
<NTabPane :name="1" :tab="$t('page.log.title')" display-directive="if">
<NDataTable
:columns="columns"
:data="data"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
</NTabPane>
</NTabs>
<LogDrawer v-model="logList" v-model:show="logShow" title="日志" />
</DetailDrawer>
</template>
<style scoped></style>