feat(report): add 24h slow-sql dashboard and metrics api

This commit is contained in:
2026-02-07 14:07:07 +08:00
parent 52dd7ac9e5
commit 6a9858cdec
29 changed files with 427 additions and 57 deletions

View File

@@ -17,7 +17,7 @@ import {
import { fetchFeedbackStats } from '../api/feedbacks'
import { fetchEmailStats } from '../api/email'
import { fetchDockerStats, fetchRequestMetrics, fetchRunningTasks, fetchServerInfo, fetchTaskStats } from '../api/tasks'
import { fetchDockerStats, fetchRequestMetrics, fetchRunningTasks, fetchServerInfo, fetchSlowSqlMetrics, fetchTaskStats } from '../api/tasks'
import { fetchBrowserPoolStats } from '../api/browser_pool'
import { fetchSystemConfig } from '../api/system'
import MetricGrid from '../components/MetricGrid.vue'
@@ -38,7 +38,9 @@ const dockerStats = ref(null)
const browserPoolStats = ref(null)
const systemConfig = ref(null)
const requestMetrics = ref(null)
const slowSqlMetrics = ref(null)
const requestDetailsOpen = ref(false)
const slowSqlDetailsOpen = ref(false)
const queueTab = ref('running')
function recordUpdatedAt() {
@@ -350,6 +352,66 @@ const requestModuleDesc = computed(() => {
return `均值 ${avgMs} · 峰值 ${maxMs} · 慢阈 ${slowThreshold} · 最近 ${lastAt}`
})
const slowSqlTopRows = computed(() => {
const rows = Array.isArray(slowSqlMetrics.value?.top_sql) ? slowSqlMetrics.value.top_sql : []
return rows.slice(0, 3)
})
const slowSqlWindowHours = computed(() => {
const seconds = normalizeCount(slowSqlMetrics.value?.window_seconds)
if (seconds <= 0) return 24
return Math.max(1, Math.round(seconds / 3600))
})
const slowSqlModuleItems = computed(() => {
const items = [
{ label: `慢SQL(${slowSqlWindowHours.value}h)`, value: normalizeCount(slowSqlMetrics.value?.total_slow_queries) },
{ label: '去重SQL', value: normalizeCount(slowSqlMetrics.value?.unique_sql) },
{ label: '平均耗时', value: formatMs(slowSqlMetrics.value?.avg_duration_ms) },
{ label: '峰值耗时', value: formatMs(slowSqlMetrics.value?.max_duration_ms) },
]
slowSqlTopRows.value.forEach((row, index) => {
items.push({
label: `慢SQL${index + 1}`,
value: `${formatMs(row?.max_ms)} · ${String(row?.sql || '-')}`,
})
})
return items
})
const slowSqlModuleDesc = computed(() => {
const threshold = formatMs(slowSqlMetrics.value?.slow_threshold_ms)
const lastAt = formatUnixTime(slowSqlMetrics.value?.last_slow_ts)
return `窗口 ${slowSqlWindowHours.value}h · 慢阈 ${threshold} · 最近 ${lastAt}`
})
const slowSqlDetailTopRows = computed(() => {
const rows = Array.isArray(slowSqlMetrics.value?.top_sql) ? slowSqlMetrics.value.top_sql : []
return rows.map((row, index) => ({
rank: index + 1,
sql: String(row?.sql || '-'),
count: normalizeCount(row?.count),
avg_ms: formatMs(row?.avg_ms),
max_ms: formatMs(row?.max_ms),
last_seen: formatUnixDateTime(row?.last_ts),
sample_params: String(row?.sample_params || '-'),
}))
})
const slowSqlRecentRows = computed(() => {
const rows = Array.isArray(slowSqlMetrics.value?.recent_slow_sql) ? slowSqlMetrics.value.recent_slow_sql : []
return [...rows]
.sort((a, b) => Number(b?.time || 0) - Number(a?.time || 0))
.map((row) => ({
time_text: formatUnixDateTime(row?.time),
sql: String(row?.sql || '-'),
duration_ms: formatMs(row?.duration_ms),
params: String(row?.params || '-'),
}))
})
const requestDetailTopPaths = computed(() => {
const rows = Array.isArray(requestMetrics.value?.top_paths) ? requestMetrics.value.top_paths : []
return rows.map((row, index) => ({
@@ -387,6 +449,10 @@ function openRequestDetails() {
requestDetailsOpen.value = true
}
function openSlowSqlDetails() {
slowSqlDetailsOpen.value = true
}
const configModuleItems = computed(() => [
{ label: '定时任务', value: scheduleEnabled.value ? '启用' : '关闭' },
{ label: '执行时间', value: scheduleTime.value || '-' },
@@ -448,6 +514,13 @@ const mobileModules = computed(() => [
tone: 'purple',
items: requestModuleItems.value,
},
{
key: 'slow_sql',
title: '慢SQL监控',
desc: slowSqlModuleDesc.value,
tone: 'red',
items: slowSqlModuleItems.value,
},
{
key: 'worker',
title: '截图线程池',
@@ -481,6 +554,7 @@ async function refreshAll(options = {}) {
dockerResult,
browserPoolResult,
requestMetricsResult,
slowSqlResult,
configResult,
] = await Promise.allSettled([
fetchTaskStats(),
@@ -491,6 +565,7 @@ async function refreshAll(options = {}) {
fetchDockerStats(),
fetchBrowserPoolStats(),
fetchRequestMetrics(),
fetchSlowSqlMetrics(),
fetchSystemConfig(),
])
@@ -502,6 +577,7 @@ async function refreshAll(options = {}) {
if (dockerResult.status === 'fulfilled') dockerStats.value = dockerResult.value
if (browserPoolResult.status === 'fulfilled') browserPoolStats.value = browserPoolResult.value
if (requestMetricsResult.status === 'fulfilled') requestMetrics.value = requestMetricsResult.value
if (slowSqlResult.status === 'fulfilled') slowSqlMetrics.value = slowSqlResult.value
if (configResult.status === 'fulfilled') systemConfig.value = configResult.value
await refreshStats?.()
@@ -599,8 +675,15 @@ onUnmounted(() => {
<div class="mobile-metric-value">{{ item.value }}</div>
</div>
</div>
<div v-if="module.key === 'request'" class="module-extra-actions">
<el-button size="small" type="primary" plain @click="openRequestDetails">查看慢接口详情</el-button>
<div v-if="module.key === 'request' || module.key === 'slow_sql'" class="module-extra-actions">
<el-button
size="small"
type="primary"
plain
@click="module.key === 'request' ? openRequestDetails() : openSlowSqlDetails()"
>
{{ module.key === 'request' ? '查看慢接口详情' : '查看慢SQL详情' }}
</el-button>
</div>
</el-card>
</section>
@@ -644,6 +727,43 @@ onUnmounted(() => {
</div>
</div>
</el-dialog>
<el-dialog v-model="slowSqlDetailsOpen" title="慢SQL详情近24小时" width="min(1080px, 96vw)">
<div class="request-dialog-summary app-muted">
<span>慢SQL总数{{ normalizeCount(slowSqlMetrics?.total_slow_queries) }}</span>
<span>去重SQL{{ normalizeCount(slowSqlMetrics?.unique_sql) }}</span>
<span>平均耗时{{ formatMs(slowSqlMetrics?.avg_duration_ms) }}</span>
<span>峰值耗时{{ formatMs(slowSqlMetrics?.max_duration_ms) }}</span>
<span>慢阈值{{ formatMs(slowSqlMetrics?.slow_threshold_ms) }}</span>
</div>
<div class="request-dialog-block">
<div class="request-dialog-title">TOP 慢SQL按出现次数</div>
<div class="table-wrap">
<el-table :data="slowSqlDetailTopRows" size="small" max-height="320">
<el-table-column prop="rank" label="#" width="60" />
<el-table-column prop="sql" label="SQL" min-width="400" show-overflow-tooltip />
<el-table-column prop="count" label="次数" width="90" />
<el-table-column prop="avg_ms" label="平均耗时" width="120" />
<el-table-column prop="max_ms" label="峰值耗时" width="120" />
<el-table-column prop="last_seen" label="最近出现" width="180" />
<el-table-column prop="sample_params" label="参数样本" min-width="140" show-overflow-tooltip />
</el-table>
</div>
</div>
<div class="request-dialog-block">
<div class="request-dialog-title">最近慢SQL</div>
<div class="table-wrap">
<el-table :data="slowSqlRecentRows" size="small" max-height="320">
<el-table-column prop="time_text" label="时间" width="180" />
<el-table-column prop="sql" label="SQL" min-width="420" show-overflow-tooltip />
<el-table-column prop="duration_ms" label="耗时" width="110" />
<el-table-column prop="params" label="参数" min-width="130" show-overflow-tooltip />
</el-table>
</div>
</div>
</el-dialog>
</div>
</template>