feat(ops): 看板上应用指标阈值显示
- 在OpsDashboard中加载阈值配置 - 在OpsDashboardHeader中根据阈值判断指标是否超标 - 超出阈值的指标显示为红色(SLA低于阈值也显示红色) - 用Icon组件替换emoji表情
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user