feat: 接入系统日志页面
This commit is contained in:
parent
d2af27dd9a
commit
3f101ac528
@ -59,6 +59,7 @@ const SUPPORTED_ADMIN_MENU_NAMES = new Set([
|
||||
'SystemKtTableDemoCreate',
|
||||
'SystemKtTableDemoDelete',
|
||||
'SystemKtTableDemoEdit',
|
||||
'SystemLog',
|
||||
'SystemMenu',
|
||||
'SystemMenuCreate',
|
||||
'SystemMenuDelete',
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export * from './dept';
|
||||
export * from './dict';
|
||||
export * from './log';
|
||||
export * from './menu';
|
||||
export * from './role';
|
||||
export * from './user';
|
||||
|
||||
71
apps/web-antdv-next/src/api/system/log.ts
Normal file
71
apps/web-antdv-next/src/api/system/log.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemLogApi {
|
||||
export type LogLevel = 'critical' | 'debug' | 'error' | 'info' | 'warning';
|
||||
|
||||
export interface LogItem {
|
||||
context?: string;
|
||||
durationMs?: number;
|
||||
hostname?: string;
|
||||
id: string;
|
||||
level: LogLevel | string;
|
||||
message: string;
|
||||
method?: string;
|
||||
path?: string;
|
||||
raw: string;
|
||||
requestId?: string;
|
||||
statusCode?: number;
|
||||
timestamp: string;
|
||||
timestampNs: string;
|
||||
}
|
||||
|
||||
export interface LogSummary {
|
||||
count: number;
|
||||
level: LogLevel;
|
||||
}
|
||||
|
||||
export interface LogStatus {
|
||||
app: string;
|
||||
configured: boolean;
|
||||
env: string;
|
||||
host?: string;
|
||||
selector: string;
|
||||
}
|
||||
|
||||
export interface PageResult<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
}
|
||||
}
|
||||
|
||||
async function getSystemLogList(params: Recordable<any>) {
|
||||
return requestClient.get<SystemLogApi.PageResult<SystemLogApi.LogItem>>(
|
||||
'/system/logs',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
async function getSystemLogSummary(params: Recordable<any>) {
|
||||
return requestClient.get<SystemLogApi.LogSummary[]>('/system/logs/summary', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
async function getSystemLogLevels() {
|
||||
return requestClient.get<
|
||||
Array<{ label: string; value: SystemLogApi.LogLevel }>
|
||||
>('/system/logs/levels');
|
||||
}
|
||||
|
||||
async function getSystemLogStatus() {
|
||||
return requestClient.get<SystemLogApi.LogStatus>('/system/logs/status');
|
||||
}
|
||||
|
||||
export {
|
||||
getSystemLogLevels,
|
||||
getSystemLogList,
|
||||
getSystemLogStatus,
|
||||
getSystemLogSummary,
|
||||
};
|
||||
@ -85,6 +85,32 @@
|
||||
"ktTableDemo": {
|
||||
"title": "Table Demo"
|
||||
},
|
||||
"log": {
|
||||
"app": "App",
|
||||
"configured": "Loki Connected",
|
||||
"context": "Context",
|
||||
"durationMs": "Duration",
|
||||
"emptyStatus": "Loki query host is not configured",
|
||||
"env": "Env",
|
||||
"detail": "Detail",
|
||||
"host": "Loki Host",
|
||||
"keyword": "Keyword",
|
||||
"level": "Level",
|
||||
"message": "Message",
|
||||
"method": "Method",
|
||||
"path": "Path",
|
||||
"raw": "Raw Log",
|
||||
"rangeMinutes": "Last N Minutes",
|
||||
"requestId": "Request ID",
|
||||
"selector": "Selector",
|
||||
"statusCode": "Status",
|
||||
"summary": "Level Summary",
|
||||
"time": "Time",
|
||||
"timeRange": "Time Range",
|
||||
"title": "System Logs",
|
||||
"total": "Total",
|
||||
"unconfigured": "Loki Not Configured"
|
||||
},
|
||||
"role": {
|
||||
"title": "Role Management",
|
||||
"list": "Role List",
|
||||
|
||||
@ -87,6 +87,32 @@
|
||||
"ktTableDemo": {
|
||||
"title": "表格演示"
|
||||
},
|
||||
"log": {
|
||||
"app": "应用",
|
||||
"configured": "Loki 已接入",
|
||||
"context": "上下文",
|
||||
"durationMs": "耗时",
|
||||
"emptyStatus": "未配置 Loki 查询地址",
|
||||
"env": "环境",
|
||||
"detail": "详情",
|
||||
"host": "Loki 地址",
|
||||
"keyword": "关键字",
|
||||
"level": "级别",
|
||||
"message": "消息",
|
||||
"method": "方法",
|
||||
"path": "路径",
|
||||
"raw": "原始日志",
|
||||
"rangeMinutes": "近 N 分钟",
|
||||
"requestId": "请求 ID",
|
||||
"selector": "选择器",
|
||||
"statusCode": "状态码",
|
||||
"summary": "级别统计",
|
||||
"time": "时间",
|
||||
"timeRange": "时间范围",
|
||||
"title": "系统日志",
|
||||
"total": "总量",
|
||||
"unconfigured": "Loki 未配置"
|
||||
},
|
||||
"role": {
|
||||
"title": "角色管理",
|
||||
"list": "角色列表",
|
||||
|
||||
@ -57,6 +57,15 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
component: () => import('#/views/system/dept/list.vue'),
|
||||
},
|
||||
{
|
||||
path: '/system/logs',
|
||||
name: 'SystemLog',
|
||||
meta: {
|
||||
icon: 'lucide:scroll-text',
|
||||
title: $t('system.log.title'),
|
||||
},
|
||||
component: () => import('#/views/system/log/list.vue'),
|
||||
},
|
||||
{
|
||||
path: '/system/ktTableDemo',
|
||||
name: 'SystemKtTableDemo',
|
||||
|
||||
435
apps/web-antdv-next/src/views/system/log/list.vue
Normal file
435
apps/web-antdv-next/src/views/system/log/list.vue
Normal file
@ -0,0 +1,435 @@
|
||||
<script lang="ts" setup>
|
||||
import type { TableColumnType } from 'antdv-next';
|
||||
|
||||
import type { SystemLogApi } from '#/api/system/log';
|
||||
import type {
|
||||
KtTableApi,
|
||||
KtTableContext,
|
||||
KtTablePageResult,
|
||||
KtTableRowAction,
|
||||
} from '#/components/ktTable';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Drawer, Tag } from 'antdv-next';
|
||||
|
||||
import {
|
||||
getSystemLogLevels,
|
||||
getSystemLogList,
|
||||
getSystemLogStatus,
|
||||
getSystemLogSummary,
|
||||
} from '#/api/system/log';
|
||||
import { KtTable, useKtTable } from '#/components/ktTable';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const levelColorMap: Record<string, string> = {
|
||||
critical: 'magenta',
|
||||
debug: 'default',
|
||||
error: 'error',
|
||||
info: 'processing',
|
||||
warning: 'warning',
|
||||
};
|
||||
|
||||
const fallbackLevelOptions: Array<{
|
||||
label: string;
|
||||
value: SystemLogApi.LogLevel;
|
||||
}> = [
|
||||
{ label: 'debug', value: 'debug' },
|
||||
{ label: 'info', value: 'info' },
|
||||
{ label: 'warning', value: 'warning' },
|
||||
{ label: 'error', value: 'error' },
|
||||
{ label: 'critical', value: 'critical' },
|
||||
];
|
||||
|
||||
const levelOptions = ref(fallbackLevelOptions);
|
||||
const summary = ref<SystemLogApi.LogSummary[]>([]);
|
||||
const status = ref<SystemLogApi.LogStatus>();
|
||||
const detailOpen = ref(false);
|
||||
const detailRecord = ref<SystemLogApi.LogItem>();
|
||||
|
||||
const summaryTotal = computed(() =>
|
||||
summary.value.reduce((total, item) => total + Number(item.count || 0), 0),
|
||||
);
|
||||
|
||||
const columns: Array<TableColumnType<SystemLogApi.LogItem>> = [
|
||||
{
|
||||
dataIndex: 'timestamp',
|
||||
fixed: 'left',
|
||||
key: 'timestamp',
|
||||
title: $t('system.log.time'),
|
||||
width: 190,
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
dataIndex: 'level',
|
||||
key: 'level',
|
||||
title: $t('system.log.level'),
|
||||
width: 110,
|
||||
},
|
||||
{
|
||||
dataIndex: 'message',
|
||||
key: 'message',
|
||||
title: $t('system.log.message'),
|
||||
width: 420,
|
||||
},
|
||||
{
|
||||
dataIndex: 'context',
|
||||
key: 'context',
|
||||
title: $t('system.log.context'),
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
dataIndex: 'method',
|
||||
key: 'method',
|
||||
title: $t('system.log.method'),
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
dataIndex: 'path',
|
||||
key: 'path',
|
||||
title: $t('system.log.path'),
|
||||
width: 260,
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
dataIndex: 'statusCode',
|
||||
key: 'statusCode',
|
||||
title: $t('system.log.statusCode'),
|
||||
width: 110,
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
dataIndex: 'durationMs',
|
||||
key: 'durationMs',
|
||||
title: $t('system.log.durationMs'),
|
||||
width: 110,
|
||||
},
|
||||
{
|
||||
dataIndex: 'requestId',
|
||||
key: 'requestId',
|
||||
title: $t('system.log.requestId'),
|
||||
width: 220,
|
||||
},
|
||||
];
|
||||
|
||||
const api: KtTableApi<SystemLogApi.LogItem> = {
|
||||
list: async (params) => await getSystemLogList(params),
|
||||
};
|
||||
|
||||
const rowActions: Array<KtTableRowAction<SystemLogApi.LogItem>> = [
|
||||
{
|
||||
key: 'detail',
|
||||
label: $t('system.log.detail'),
|
||||
onClick: onDetail,
|
||||
permissionCodes: ['System:Log:List'],
|
||||
},
|
||||
];
|
||||
|
||||
const [registerTable] = useKtTable<SystemLogApi.LogItem>({
|
||||
afterFetch: onAfterFetch,
|
||||
api,
|
||||
columns,
|
||||
formOptions: {
|
||||
fieldMappingTime: [
|
||||
['logTime', ['startTime', 'endTime'], 'YYYY-MM-DD HH:mm:ss'],
|
||||
],
|
||||
schema: [
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: () => ({
|
||||
allowClear: true,
|
||||
options: levelOptions.value,
|
||||
}),
|
||||
fieldName: 'level',
|
||||
label: $t('system.log.level'),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'keyword',
|
||||
label: $t('system.log.keyword'),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'context',
|
||||
label: $t('system.log.context'),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'path',
|
||||
label: $t('system.log.path'),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'requestId',
|
||||
label: $t('system.log.requestId'),
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
fieldName: 'logTime',
|
||||
label: $t('system.log.timeRange'),
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
min: 1,
|
||||
precision: 0,
|
||||
},
|
||||
defaultValue: 60,
|
||||
fieldName: 'rangeMinutes',
|
||||
label: $t('system.log.rangeMinutes'),
|
||||
},
|
||||
],
|
||||
},
|
||||
pageSize: 20,
|
||||
rowActions,
|
||||
rowKey: 'id',
|
||||
showSelection: false,
|
||||
tableTitle: $t('system.log.title'),
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadStatus(), loadLevels(), refreshSummary()]);
|
||||
});
|
||||
|
||||
function getLevelColor(level: string) {
|
||||
return levelColorMap[level] || 'default';
|
||||
}
|
||||
|
||||
function getStatusColor(statusCode?: number) {
|
||||
if (!statusCode) return 'default';
|
||||
if (statusCode >= 500) return 'error';
|
||||
if (statusCode >= 400) return 'warning';
|
||||
if (statusCode >= 300) return 'processing';
|
||||
return 'success';
|
||||
}
|
||||
|
||||
function getSummaryCount(level: string) {
|
||||
return summary.value.find((item) => item.level === level)?.count || 0;
|
||||
}
|
||||
|
||||
async function loadStatus() {
|
||||
status.value = await getSystemLogStatus();
|
||||
}
|
||||
|
||||
async function loadLevels() {
|
||||
const options = await getSystemLogLevels();
|
||||
levelOptions.value = options.length > 0 ? options : fallbackLevelOptions;
|
||||
}
|
||||
|
||||
async function refreshSummary(params: Record<string, any> = {}) {
|
||||
summary.value = await getSystemLogSummary(params);
|
||||
}
|
||||
|
||||
async function onAfterFetch(
|
||||
result: KtTablePageResult<SystemLogApi.LogItem> | SystemLogApi.LogItem[],
|
||||
context: KtTableContext<SystemLogApi.LogItem>,
|
||||
) {
|
||||
await refreshSummary(await context.getSearchValues());
|
||||
return result;
|
||||
}
|
||||
|
||||
function onDetail(row: SystemLogApi.LogItem) {
|
||||
detailRecord.value = row;
|
||||
detailOpen.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="system-log-page">
|
||||
<section class="system-log-page__status">
|
||||
<div class="system-log-page__status-main">
|
||||
<Tag :color="status?.configured ? 'success' : 'warning'">
|
||||
{{
|
||||
status?.configured
|
||||
? $t('system.log.configured')
|
||||
: $t('system.log.unconfigured')
|
||||
}}
|
||||
</Tag>
|
||||
<span>{{ status?.app || '-' }}</span>
|
||||
<span>{{ status?.env || '-' }}</span>
|
||||
<span class="system-log-page__muted">
|
||||
{{ status?.selector || $t('system.log.emptyStatus') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="system-log-page__host">
|
||||
{{ status?.host || '-' }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="system-log-page__summary">
|
||||
<div class="system-log-page__summary-item">
|
||||
<span>{{ $t('system.log.total') }}</span>
|
||||
<strong>{{ summaryTotal }}</strong>
|
||||
</div>
|
||||
<div
|
||||
v-for="item in levelOptions"
|
||||
:key="item.value"
|
||||
class="system-log-page__summary-item"
|
||||
>
|
||||
<Tag :color="getLevelColor(item.value)">
|
||||
{{ item.label }}
|
||||
</Tag>
|
||||
<strong>{{ getSummaryCount(item.value) }}</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<KtTable @register="registerTable">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'level'">
|
||||
<Tag :color="getLevelColor(record.level)">
|
||||
{{ record.level }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'statusCode'">
|
||||
<Tag :color="getStatusColor(record.statusCode)">
|
||||
{{ record.statusCode || '-' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'durationMs'">
|
||||
{{
|
||||
record.durationMs === undefined ? '-' : `${record.durationMs} ms`
|
||||
}}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'message'">
|
||||
<span class="system-log-page__message" :title="record.message">
|
||||
{{ record.message }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</KtTable>
|
||||
</div>
|
||||
|
||||
<Drawer v-model:open="detailOpen" :size="720" :title="$t('system.log.raw')">
|
||||
<dl v-if="detailRecord" class="system-log-page__detail">
|
||||
<dt>{{ $t('system.log.time') }}</dt>
|
||||
<dd>{{ detailRecord.timestamp }}</dd>
|
||||
<dt>{{ $t('system.log.level') }}</dt>
|
||||
<dd>
|
||||
<Tag :color="getLevelColor(detailRecord.level)">
|
||||
{{ detailRecord.level }}
|
||||
</Tag>
|
||||
</dd>
|
||||
<dt>{{ $t('system.log.context') }}</dt>
|
||||
<dd>{{ detailRecord.context || '-' }}</dd>
|
||||
<dt>{{ $t('system.log.requestId') }}</dt>
|
||||
<dd>{{ detailRecord.requestId || '-' }}</dd>
|
||||
<dt>{{ $t('system.log.path') }}</dt>
|
||||
<dd>{{ detailRecord.path || '-' }}</dd>
|
||||
</dl>
|
||||
<pre class="system-log-page__raw">{{ detailRecord?.raw || '' }}</pre>
|
||||
</Drawer>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.system-log-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.system-log-page__status,
|
||||
.system-log-page__summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
background: hsl(var(--background));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.system-log-page__status {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.system-log-page__status-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.system-log-page__host,
|
||||
.system-log-page__muted {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.system-log-page__summary-item {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-height: 30px;
|
||||
padding: 0 10px;
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.system-log-page__summary-item:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.system-log-page__summary-item strong {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.system-log-page__message {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.system-log-page__detail {
|
||||
display: grid;
|
||||
grid-template-columns: 96px minmax(0, 1fr);
|
||||
gap: 10px 12px;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.system-log-page__detail dt {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.system-log-page__detail dd {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.system-log-page__raw {
|
||||
min-height: 280px;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
background: hsl(var(--muted));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user