feat(ops): 看板上应用指标阈值显示

- 在OpsDashboard中加载阈值配置
- 在OpsDashboardHeader中根据阈值判断指标是否超标
- 超出阈值的指标显示为红色(SLA低于阈值也显示红色)
- 用Icon组件替换emoji表情
This commit is contained in:
IanShaw027
2026-01-12 11:44:14 +08:00
parent d0b91a40d4
commit 72a2ed958b
2 changed files with 75 additions and 14 deletions

View File

@@ -24,6 +24,7 @@
:query-mode="queryMode"
:loading="loading"
:last-updated="lastUpdated"
:thresholds="metricThresholds"
@update:time-range="onTimeRangeChange"
@update:platform="onPlatformChange"
@update:group="onGroupChange"
@@ -75,7 +76,7 @@
<OpsAlertEventsCard v-if="opsEnabled && !(loading && !hasLoadedOnce)" />
<!-- Settings Dialog -->
<OpsSettingsDialog :show="showSettingsDialog" @close="showSettingsDialog = false" @saved="fetchData" />
<OpsSettingsDialog :show="showSettingsDialog" @close="showSettingsDialog = false" @saved="onSettingsSaved" />
<!-- Alert Rules Dialog -->
<BaseDialog :show="showAlertRulesCard" :title="t('admin.ops.alertRules.title')" width="extra-wide" @close="showAlertRulesCard = false">
@@ -121,7 +122,8 @@ import {
type OpsErrorDistributionResponse,
type OpsErrorTrendResponse,
type OpsLatencyHistogramResponse,
type OpsThroughputTrendResponse
type OpsThroughputTrendResponse,
type OpsMetricThresholds
} from '@/api/admin/ops'
import { useAdminSettingsStore, useAppStore } from '@/stores'
import OpsDashboardHeader from './components/OpsDashboardHeader.vue'
@@ -314,6 +316,7 @@ const syncQueryToRoute = useDebounceFn(async () => {
}, 250)
const overview = ref<OpsDashboardOverview | null>(null)
const metricThresholds = ref<OpsMetricThresholds | null>(null)
const throughputTrend = ref<OpsThroughputTrendResponse | null>(null)
const loadingTrend = ref(false)
@@ -376,6 +379,11 @@ function onTimeRangeChange(v: string | number | boolean | null) {
timeRange.value = v as TimeRange
}
function onSettingsSaved() {
loadThresholds()
fetchData()
}
function onPlatformChange(v: string | number | boolean | null) {
platform.value = typeof v === 'string' ? v : ''
}
@@ -615,6 +623,9 @@ onMounted(async () => {
return
}
// Load thresholds configuration
loadThresholds()
if (adminSettingsStore.opsRealtimeMonitoringEnabled) {
startQPSSubscription()
} else {
@@ -626,6 +637,16 @@ onMounted(async () => {
}
})
async function loadThresholds() {
try {
const settings = await opsAPI.getAlertRuntimeSettings()
metricThresholds.value = settings.thresholds || null
} catch (err) {
console.warn('[OpsDashboard] Failed to load thresholds', err)
metricThresholds.value = null
}
}
onUnmounted(() => {
stopQPSSubscription()
abortDashboardFetch()

View File

@@ -4,8 +4,9 @@ import { useI18n } from 'vue-i18n'
import Select from '@/components/common/Select.vue'
import HelpTooltip from '@/components/common/HelpTooltip.vue'
import BaseDialog from '@/components/common/BaseDialog.vue'
import Icon from '@/components/icons/Icon.vue'
import { adminAPI } from '@/api'
import type { OpsDashboardOverview, OpsWSStatus } from '@/api/admin/ops'
import type { OpsDashboardOverview, OpsWSStatus, OpsMetricThresholds } from '@/api/admin/ops'
import type { OpsRequestDetailsPreset } from './OpsRequestDetailsModal.vue'
import { formatNumber } from '@/utils/format'
@@ -24,6 +25,7 @@ interface Props {
queryMode: string
loading: boolean
lastUpdated: Date | null
thresholds?: OpsMetricThresholds | null // 阈值配置
}
interface Emits {
@@ -143,6 +145,42 @@ function getLatencyColor(ms: number | null | undefined): string {
return 'text-red-600 dark:text-red-400'
}
// --- Threshold checking helpers ---
function isSLABelowThreshold(slaPercent: number | null): boolean {
if (slaPercent == null) return false
const threshold = props.thresholds?.sla_percent_min
if (threshold == null) return false
return slaPercent < threshold
}
function isLatencyAboveThreshold(latencyP99Ms: number | null): boolean {
if (latencyP99Ms == null) return false
const threshold = props.thresholds?.latency_p99_ms_max
if (threshold == null) return false
return latencyP99Ms > threshold
}
function isTTFTAboveThreshold(ttftP99Ms: number | null): boolean {
if (ttftP99Ms == null) return false
const threshold = props.thresholds?.ttft_p99_ms_max
if (threshold == null) return false
return ttftP99Ms > threshold
}
function isRequestErrorRateAboveThreshold(errorRatePercent: number | null): boolean {
if (errorRatePercent == null) return false
const threshold = props.thresholds?.request_error_rate_percent_max
if (threshold == null) return false
return errorRatePercent > threshold
}
function isUpstreamErrorRateAboveThreshold(upstreamErrorRatePercent: number | null): boolean {
if (upstreamErrorRatePercent == null) return false
const threshold = props.thresholds?.upstream_error_rate_percent_max
if (threshold == null) return false
return upstreamErrorRatePercent > threshold
}
// --- Realtime / Overview labels ---
const totalRequestsLabel = computed(() => formatNumber(overview.value?.request_count_total ?? 0))
@@ -818,8 +856,9 @@ function openJobsDetails() {
class="pointer-events-none absolute left-1/2 top-full z-50 mt-2 w-72 -translate-x-1/2 opacity-0 transition-opacity duration-200 group-hover:pointer-events-auto group-hover:opacity-100 md:left-full md:top-0 md:ml-2 md:mt-0 md:translate-x-0"
>
<div class="rounded-xl bg-white p-4 shadow-xl ring-1 ring-black/5 dark:bg-gray-800 dark:ring-white/10">
<h4 class="mb-3 border-b border-gray-100 pb-2 text-sm font-bold text-gray-900 dark:border-gray-700 dark:text-white">
🧠 {{ t('admin.ops.diagnosis.title') }}
<h4 class="mb-3 border-b border-gray-100 pb-2 text-sm font-bold text-gray-900 dark:border-gray-700 dark:text-white flex items-center gap-2">
<Icon name="brain" size="sm" class="text-blue-500" />
{{ t('admin.ops.diagnosis.title') }}
</h4>
<div class="space-y-3">
@@ -850,8 +889,9 @@ function openJobsDetails() {
<div class="flex-1">
<div class="text-xs font-semibold text-gray-900 dark:text-white">{{ item.message }}</div>
<div class="mt-0.5 text-[11px] text-gray-500 dark:text-gray-400">{{ item.impact }}</div>
<div v-if="item.action" class="mt-1 text-[11px] text-blue-600 dark:text-blue-400">
💡 {{ item.action }}
<div v-if="item.action" class="mt-1 text-[11px] text-blue-600 dark:text-blue-400 flex items-center gap-1">
<Icon name="lightbulb" size="xs" />
{{ item.action }}
</div>
</div>
</div>
@@ -1061,7 +1101,7 @@ function openJobsDetails() {
<div class="flex items-center gap-2">
<span class="text-[10px] font-bold uppercase text-gray-400">SLA</span>
<HelpTooltip :content="t('admin.ops.tooltips.sla')" />
<span class="h-1.5 w-1.5 rounded-full" :class="(slaPercent ?? 0) >= 99.5 ? 'bg-green-500' : 'bg-yellow-500'"></span>
<span class="h-1.5 w-1.5 rounded-full" :class="isSLABelowThreshold(slaPercent) ? 'bg-red-500' : (slaPercent ?? 0) >= 99.5 ? 'bg-green-500' : 'bg-yellow-500'"></span>
</div>
<button
class="text-[10px] font-bold text-blue-500 hover:underline"
@@ -1071,11 +1111,11 @@ function openJobsDetails() {
{{ t('admin.ops.requestDetails.details') }}
</button>
</div>
<div class="mt-2 text-3xl font-black text-gray-900 dark:text-white">
<div class="mt-2 text-3xl font-black" :class="isSLABelowThreshold(slaPercent) ? 'text-red-600 dark:text-red-400' : 'text-gray-900 dark:text-white'">
{{ slaPercent == null ? '-' : `${slaPercent.toFixed(3)}%` }}
</div>
<div class="mt-3 h-2 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-dark-700">
<div class="h-full bg-green-500 transition-all" :style="{ width: `${Math.max((slaPercent ?? 0) - 90, 0) * 10}%` }"></div>
<div class="h-full transition-all" :class="isSLABelowThreshold(slaPercent) ? 'bg-red-500' : 'bg-green-500'" :style="{ width: `${Math.max((slaPercent ?? 0) - 90, 0) * 10}%` }"></div>
</div>
<div class="mt-3 text-xs">
<div class="flex justify-between">
@@ -1101,7 +1141,7 @@ function openJobsDetails() {
</button>
</div>
<div class="mt-2 flex items-baseline gap-2">
<div class="text-3xl font-black" :class="getLatencyColor(durationP99Ms)">
<div class="text-3xl font-black" :class="isLatencyAboveThreshold(durationP99Ms) ? 'text-red-600 dark:text-red-400' : getLatencyColor(durationP99Ms)">
{{ durationP99Ms ?? '-' }}
</div>
<span class="text-xs font-bold text-gray-400">ms (P99)</span>
@@ -1151,7 +1191,7 @@ function openJobsDetails() {
</button>
</div>
<div class="mt-2 flex items-baseline gap-2">
<div class="text-3xl font-black" :class="getLatencyColor(ttftP99Ms)">
<div class="text-3xl font-black" :class="isTTFTAboveThreshold(ttftP99Ms) ? 'text-red-600 dark:text-red-400' : getLatencyColor(ttftP99Ms)">
{{ ttftP99Ms ?? '-' }}
</div>
<span class="text-xs font-bold text-gray-400">ms (P99)</span>
@@ -1196,7 +1236,7 @@ function openJobsDetails() {
{{ t('admin.ops.requestDetails.details') }}
</button>
</div>
<div class="mt-2 text-3xl font-black" :class="(errorRatePercent ?? 0) > 5 ? 'text-red-500' : 'text-gray-900 dark:text-white'">
<div class="mt-2 text-3xl font-black" :class="isRequestErrorRateAboveThreshold(errorRatePercent) ? 'text-red-600 dark:text-red-400' : (errorRatePercent ?? 0) > 5 ? 'text-red-500' : 'text-gray-900 dark:text-white'">
{{ errorRatePercent == null ? '-' : `${errorRatePercent.toFixed(2)}%` }}
</div>
<div class="mt-3 space-y-1 text-xs">
@@ -1222,7 +1262,7 @@ function openJobsDetails() {
{{ t('admin.ops.requestDetails.details') }}
</button>
</div>
<div class="mt-2 text-3xl font-black" :class="(upstreamErrorRatePercent ?? 0) > 5 ? 'text-red-500' : 'text-gray-900 dark:text-white'">
<div class="mt-2 text-3xl font-black" :class="isUpstreamErrorRateAboveThreshold(upstreamErrorRatePercent) ? 'text-red-600 dark:text-red-400' : (upstreamErrorRatePercent ?? 0) > 5 ? 'text-red-500' : 'text-gray-900 dark:text-white'">
{{ upstreamErrorRatePercent == null ? '-' : `${upstreamErrorRatePercent.toFixed(2)}%` }}
</div>
<div class="mt-3 space-y-1 text-xs">