From d0b91a40d4fc06fc50b96e6e3ef28742910d4fa2 Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:43:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(ops):=20=E6=B7=BB=E5=8A=A0=E6=8C=87?= =?UTF-8?q?=E6=A0=87=E9=98=88=E5=80=BC=E9=85=8D=E7=BD=AEUI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在OpsSettingsDialog中添加指标阈值配置表单 - 在OpsRuntimeSettingsCard中添加阈值配置区域 - 添加阈值验证逻辑 - 更新国际化文本 --- frontend/src/i18n/locales/zh.ts | 16 ++- .../ops/components/OpsRuntimeSettingsCard.vue | 114 +++++++++++++++++ .../ops/components/OpsSettingsDialog.vue | 118 +++++++++++++++++- 3 files changed, 241 insertions(+), 7 deletions(-) diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index ecdcb13f..9ccd5678 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -2018,7 +2018,7 @@ export default { ready: '就绪', requestsTotal: '请求(总计)', slaScope: 'SLA 范围:', - tokens: 'Token', + tokens: 'Token数', tps: 'TPS', current: '当前', peak: '峰值', @@ -2047,7 +2047,7 @@ export default { avg: 'avg', max: 'max', qps: 'QPS', - requests: '请求', + requests: '请求数', upstream: '上游', client: '客户端', system: '系统', @@ -2465,6 +2465,18 @@ export default { reportRecipients: '评估报告接收邮箱', dailySummary: '每日摘要', weeklySummary: '每周摘要', + metricThresholds: '指标阈值配置', + metricThresholdsHint: '配置各项指标的告警阈值,超出阈值时将以红色显示', + slaMinPercent: 'SLA最低百分比', + slaMinPercentHint: 'SLA低于此值时显示为红色(默认:99.5%)', + latencyP99MaxMs: '延迟P99最大值(毫秒)', + latencyP99MaxMsHint: '延迟P99高于此值时显示为红色(默认:2000ms)', + ttftP99MaxMs: 'TTFT P99最大值(毫秒)', + ttftP99MaxMsHint: 'TTFT P99高于此值时显示为红色(默认:500ms)', + requestErrorRateMaxPercent: '请求错误率最大值(%)', + requestErrorRateMaxPercentHint: '请求错误率高于此值时显示为红色(默认:5%)', + upstreamErrorRateMaxPercent: '上游错误率最大值(%)', + upstreamErrorRateMaxPercentHint: '上游错误率高于此值时显示为红色(默认:5%)', advancedSettings: '高级设置', dataRetention: '数据保留策略', enableCleanup: '启用数据清理', diff --git a/frontend/src/views/admin/ops/components/OpsRuntimeSettingsCard.vue b/frontend/src/views/admin/ops/components/OpsRuntimeSettingsCard.vue index e9df347d..1dcab4b3 100644 --- a/frontend/src/views/admin/ops/components/OpsRuntimeSettingsCard.vue +++ b/frontend/src/views/admin/ops/components/OpsRuntimeSettingsCard.vue @@ -45,6 +45,36 @@ function validateRuntimeSettings(settings: OpsAlertRuntimeSettings): ValidationR errors.push(t('admin.ops.runtime.validation.evalIntervalRange')) } + // Thresholds validation + const thresholds = settings.thresholds + if (thresholds) { + if (thresholds.sla_percent_min != null) { + if (!Number.isFinite(thresholds.sla_percent_min) || thresholds.sla_percent_min < 0 || thresholds.sla_percent_min > 100) { + errors.push('SLA 最低值必须在 0-100 之间') + } + } + if (thresholds.latency_p99_ms_max != null) { + if (!Number.isFinite(thresholds.latency_p99_ms_max) || thresholds.latency_p99_ms_max < 0) { + errors.push('延迟 P99 最大值必须大于或等于 0') + } + } + if (thresholds.ttft_p99_ms_max != null) { + if (!Number.isFinite(thresholds.ttft_p99_ms_max) || thresholds.ttft_p99_ms_max < 0) { + errors.push('TTFT P99 最大值必须大于或等于 0') + } + } + if (thresholds.request_error_rate_percent_max != null) { + if (!Number.isFinite(thresholds.request_error_rate_percent_max) || thresholds.request_error_rate_percent_max < 0 || thresholds.request_error_rate_percent_max > 100) { + errors.push('请求错误率最大值必须在 0-100 之间') + } + } + if (thresholds.upstream_error_rate_percent_max != null) { + if (!Number.isFinite(thresholds.upstream_error_rate_percent_max) || thresholds.upstream_error_rate_percent_max < 0 || thresholds.upstream_error_rate_percent_max > 100) { + errors.push('上游错误率最大值必须在 0-100 之间') + } + } + } + const lock = settings.distributed_lock if (lock?.enabled) { if (!lock.key || lock.key.trim().length < 3) { @@ -130,6 +160,15 @@ function openAlertEditor() { if (!Array.isArray(draftAlert.value.silencing.entries)) { draftAlert.value.silencing.entries = [] } + if (!draftAlert.value.thresholds) { + draftAlert.value.thresholds = { + sla_percent_min: 99.5, + latency_p99_ms_max: 2000, + ttft_p99_ms_max: 500, + request_error_rate_percent_max: 5, + upstream_error_rate_percent_max: 5 + } + } } showAlertEditor.value = true @@ -295,6 +334,81 @@ onMounted(() => {

{{ t('admin.ops.runtime.evalIntervalHint') }}

+
+
指标阈值配置
+

配置各项指标的告警阈值。超出阈值的指标将在看板上以红色显示。

+ +
+
+
SLA 最低值 (%)
+ +

SLA 低于此值时将显示为红色

+
+ +
+
延迟 P99 最大值 (ms)
+ +

延迟 P99 高于此值时将显示为红色

+
+ +
+
TTFT P99 最大值 (ms)
+ +

TTFT P99 高于此值时将显示为红色

+
+ +
+
请求错误率最大值 (%)
+ +

请求错误率高于此值时将显示为红色

+
+ +
+
上游错误率最大值 (%)
+ +

上游错误率高于此值时将显示为红色

+
+
+
+
{{ t('admin.ops.runtime.silencing.title') }}
diff --git a/frontend/src/views/admin/ops/components/OpsSettingsDialog.vue b/frontend/src/views/admin/ops/components/OpsSettingsDialog.vue index 968c5081..0c9c4f81 100644 --- a/frontend/src/views/admin/ops/components/OpsSettingsDialog.vue +++ b/frontend/src/views/admin/ops/components/OpsSettingsDialog.vue @@ -6,7 +6,7 @@ import { opsAPI } from '@/api/admin/ops' import BaseDialog from '@/components/common/BaseDialog.vue' import Select from '@/components/common/Select.vue' import Toggle from '@/components/common/Toggle.vue' -import type { OpsAlertRuntimeSettings, EmailNotificationConfig, AlertSeverity, OpsAdvancedSettings } from '../types' +import type { OpsAlertRuntimeSettings, EmailNotificationConfig, AlertSeverity, OpsAdvancedSettings, OpsMetricThresholds } from '../types' const { t } = useI18n() const appStore = useAppStore() @@ -29,19 +29,38 @@ const runtimeSettings = ref(null) const emailConfig = ref(null) // 高级设置 const advancedSettings = ref(null) +// 指标阈值配置 +const metricThresholds = ref({ + sla_percent_min: 99.5, + latency_p99_ms_max: 2000, + ttft_p99_ms_max: 500, + request_error_rate_percent_max: 5, + upstream_error_rate_percent_max: 5 +}) // 加载所有配置 async function loadAllSettings() { loading.value = true try { - const [runtime, email, advanced] = await Promise.all([ + const [runtime, email, advanced, thresholds] = await Promise.all([ opsAPI.getAlertRuntimeSettings(), opsAPI.getEmailNotificationConfig(), - opsAPI.getAdvancedSettings() + opsAPI.getAdvancedSettings(), + opsAPI.getMetricThresholds() ]) runtimeSettings.value = runtime emailConfig.value = email advancedSettings.value = advanced + // 如果后端返回了阈值,使用后端的值;否则保持默认值 + if (thresholds && Object.keys(thresholds).length > 0) { + metricThresholds.value = { + sla_percent_min: thresholds.sla_percent_min ?? 99.5, + latency_p99_ms_max: thresholds.latency_p99_ms_max ?? 2000, + ttft_p99_ms_max: thresholds.ttft_p99_ms_max ?? 500, + request_error_rate_percent_max: thresholds.request_error_rate_percent_max ?? 5, + upstream_error_rate_percent_max: thresholds.upstream_error_rate_percent_max ?? 5 + } + } } catch (err: any) { console.error('[OpsSettingsDialog] Failed to load settings', err) appStore.showError(err?.response?.data?.detail || t('admin.ops.settings.loadFailed')) @@ -138,6 +157,23 @@ const validation = computed(() => { } } + // 验证指标阈值 + if (metricThresholds.value.sla_percent_min != null && (metricThresholds.value.sla_percent_min < 0 || metricThresholds.value.sla_percent_min > 100)) { + errors.push('SLA最低百分比必须在0-100之间') + } + if (metricThresholds.value.latency_p99_ms_max != null && metricThresholds.value.latency_p99_ms_max < 0) { + errors.push('延迟P99最大值必须大于等于0') + } + if (metricThresholds.value.ttft_p99_ms_max != null && metricThresholds.value.ttft_p99_ms_max < 0) { + errors.push('TTFT P99最大值必须大于等于0') + } + if (metricThresholds.value.request_error_rate_percent_max != null && (metricThresholds.value.request_error_rate_percent_max < 0 || metricThresholds.value.request_error_rate_percent_max > 100)) { + errors.push('请求错误率最大值必须在0-100之间') + } + if (metricThresholds.value.upstream_error_rate_percent_max != null && (metricThresholds.value.upstream_error_rate_percent_max < 0 || metricThresholds.value.upstream_error_rate_percent_max > 100)) { + errors.push('上游错误率最大值必须在0-100之间') + } + return { valid: errors.length === 0, errors } }) @@ -153,14 +189,15 @@ async function saveAllSettings() { await Promise.all([ runtimeSettings.value ? opsAPI.updateAlertRuntimeSettings(runtimeSettings.value) : Promise.resolve(), emailConfig.value ? opsAPI.updateEmailNotificationConfig(emailConfig.value) : Promise.resolve(), - advancedSettings.value ? opsAPI.updateAdvancedSettings(advancedSettings.value) : Promise.resolve() + advancedSettings.value ? opsAPI.updateAdvancedSettings(advancedSettings.value) : Promise.resolve(), + opsAPI.updateMetricThresholds(metricThresholds.value) ]) appStore.showSuccess(t('admin.ops.settings.saveSuccess')) emit('saved') emit('close') } catch (err: any) { console.error('[OpsSettingsDialog] Failed to save settings', err) - appStore.showError(err?.response?.data?.detail || t('admin.ops.settings.saveFailed')) + appStore.showError(err?.response?.data?.message || err?.response?.data?.detail || t('admin.ops.settings.saveFailed')) } finally { saving.value = false } @@ -306,6 +343,77 @@ async function saveAllSettings() {
+ +
+

{{ t('admin.ops.settings.metricThresholds') }}

+

{{ t('admin.ops.settings.metricThresholdsHint') }}

+ +
+
+ + +

{{ t('admin.ops.settings.slaMinPercentHint') }}

+
+ +
+ + +

{{ t('admin.ops.settings.latencyP99MaxMsHint') }}

+
+ +
+ + +

{{ t('admin.ops.settings.ttftP99MaxMsHint') }}

+
+ +
+ + +

{{ t('admin.ops.settings.requestErrorRateMaxPercentHint') }}

+
+ +
+ + +

{{ t('admin.ops.settings.upstreamErrorRateMaxPercentHint') }}

+
+
+
+