feat(ops): 添加指标阈值配置UI
- 在OpsSettingsDialog中添加指标阈值配置表单 - 在OpsRuntimeSettingsCard中添加阈值配置区域 - 添加阈值验证逻辑 - 更新国际化文本
This commit is contained in:
@@ -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: '启用数据清理',
|
||||
|
||||
@@ -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(() => {
|
||||
<p class="mt-1 text-xs text-gray-500">{{ t('admin.ops.runtime.evalIntervalHint') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl bg-gray-50 p-4 dark:bg-dark-700/50">
|
||||
<div class="mb-2 text-sm font-semibold text-gray-900 dark:text-white">指标阈值配置</div>
|
||||
<p class="mb-4 text-xs text-gray-500 dark:text-gray-400">配置各项指标的告警阈值。超出阈值的指标将在看板上以红色显示。</p>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<div class="mb-1 text-xs font-medium text-gray-600 dark:text-gray-300">SLA 最低值 (%)</div>
|
||||
<input
|
||||
v-model.number="draftAlert.thresholds.sla_percent_min"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
class="input"
|
||||
placeholder="99.5"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">SLA 低于此值时将显示为红色</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-1 text-xs font-medium text-gray-600 dark:text-gray-300">延迟 P99 最大值 (ms)</div>
|
||||
<input
|
||||
v-model.number="draftAlert.thresholds.latency_p99_ms_max"
|
||||
type="number"
|
||||
min="0"
|
||||
step="100"
|
||||
class="input"
|
||||
placeholder="2000"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">延迟 P99 高于此值时将显示为红色</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-1 text-xs font-medium text-gray-600 dark:text-gray-300">TTFT P99 最大值 (ms)</div>
|
||||
<input
|
||||
v-model.number="draftAlert.thresholds.ttft_p99_ms_max"
|
||||
type="number"
|
||||
min="0"
|
||||
step="100"
|
||||
class="input"
|
||||
placeholder="500"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">TTFT P99 高于此值时将显示为红色</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-1 text-xs font-medium text-gray-600 dark:text-gray-300">请求错误率最大值 (%)</div>
|
||||
<input
|
||||
v-model.number="draftAlert.thresholds.request_error_rate_percent_max"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
class="input"
|
||||
placeholder="5"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">请求错误率高于此值时将显示为红色</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-1 text-xs font-medium text-gray-600 dark:text-gray-300">上游错误率最大值 (%)</div>
|
||||
<input
|
||||
v-model.number="draftAlert.thresholds.upstream_error_rate_percent_max"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
class="input"
|
||||
placeholder="5"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">上游错误率高于此值时将显示为红色</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl bg-gray-50 p-4 dark:bg-dark-700/50">
|
||||
<div class="mb-2 text-sm font-semibold text-gray-900 dark:text-white">{{ t('admin.ops.runtime.silencing.title') }}</div>
|
||||
|
||||
|
||||
@@ -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<OpsAlertRuntimeSettings | null>(null)
|
||||
const emailConfig = ref<EmailNotificationConfig | null>(null)
|
||||
// 高级设置
|
||||
const advancedSettings = ref<OpsAdvancedSettings | null>(null)
|
||||
// 指标阈值配置
|
||||
const metricThresholds = ref<OpsMetricThresholds>({
|
||||
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() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 指标阈值配置 -->
|
||||
<div class="rounded-2xl bg-gray-50 p-4 dark:bg-dark-700/50">
|
||||
<h4 class="mb-3 text-sm font-semibold text-gray-900 dark:text-white">{{ t('admin.ops.settings.metricThresholds') }}</h4>
|
||||
<p class="mb-4 text-xs text-gray-500 dark:text-gray-400">{{ t('admin.ops.settings.metricThresholdsHint') }}</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.ops.settings.slaMinPercent') }}</label>
|
||||
<input
|
||||
v-model.number="metricThresholds.sla_percent_min"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
class="input"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">{{ t('admin.ops.settings.slaMinPercentHint') }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.ops.settings.latencyP99MaxMs') }}</label>
|
||||
<input
|
||||
v-model.number="metricThresholds.latency_p99_ms_max"
|
||||
type="number"
|
||||
min="0"
|
||||
step="100"
|
||||
class="input"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">{{ t('admin.ops.settings.latencyP99MaxMsHint') }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.ops.settings.ttftP99MaxMs') }}</label>
|
||||
<input
|
||||
v-model.number="metricThresholds.ttft_p99_ms_max"
|
||||
type="number"
|
||||
min="0"
|
||||
step="50"
|
||||
class="input"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">{{ t('admin.ops.settings.ttftP99MaxMsHint') }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.ops.settings.requestErrorRateMaxPercent') }}</label>
|
||||
<input
|
||||
v-model.number="metricThresholds.request_error_rate_percent_max"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
class="input"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">{{ t('admin.ops.settings.requestErrorRateMaxPercentHint') }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.ops.settings.upstreamErrorRateMaxPercent') }}</label>
|
||||
<input
|
||||
v-model.number="metricThresholds.upstream_error_rate_percent_max"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
class="input"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">{{ t('admin.ops.settings.upstreamErrorRateMaxPercentHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高级设置 -->
|
||||
<details class="rounded-2xl bg-gray-50 dark:bg-dark-700/50">
|
||||
<summary class="cursor-pointer p-4 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
|
||||
Reference in New Issue
Block a user