From 7536dbfee5478f0f7922d904e826883af284e019 Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:42:56 +0800 Subject: [PATCH 01/14] =?UTF-8?q?feat(ops):=20=E5=90=8E=E7=AB=AF=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=8C=87=E6=A0=87=E9=98=88=E5=80=BC=E7=AE=A1=E7=90=86?= =?UTF-8?q?API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增GetMetricThresholds和UpdateMetricThresholds接口 - 支持配置SLA、延迟P99、TTFT P99、请求错误率、上游错误率阈值 - 添加参数验证逻辑 - 提供默认阈值配置 --- .../handler/admin/ops_settings_handler.go | 47 ++++++++++ backend/internal/server/routes/admin.go | 7 ++ backend/internal/service/ops_settings.go | 91 +++++++++++++++++++ .../internal/service/ops_settings_models.go | 9 ++ 4 files changed, 154 insertions(+) diff --git a/backend/internal/handler/admin/ops_settings_handler.go b/backend/internal/handler/admin/ops_settings_handler.go index 0e0ecb72..982836d0 100644 --- a/backend/internal/handler/admin/ops_settings_handler.go +++ b/backend/internal/handler/admin/ops_settings_handler.go @@ -146,3 +146,50 @@ func (h *OpsHandler) UpdateAdvancedSettings(c *gin.Context) { } response.Success(c, updated) } + +// GetMetricThresholds returns Ops metric thresholds (DB-backed). +// GET /api/v1/admin/ops/settings/metric-thresholds +func (h *OpsHandler) GetMetricThresholds(c *gin.Context) { + if h.opsService == nil { + response.Error(c, http.StatusServiceUnavailable, "Ops service not available") + return + } + if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil { + response.ErrorFrom(c, err) + return + } + + cfg, err := h.opsService.GetMetricThresholds(c.Request.Context()) + if err != nil { + response.Error(c, http.StatusInternalServerError, "Failed to get metric thresholds") + return + } + response.Success(c, cfg) +} + +// UpdateMetricThresholds updates Ops metric thresholds (DB-backed). +// PUT /api/v1/admin/ops/settings/metric-thresholds +func (h *OpsHandler) UpdateMetricThresholds(c *gin.Context) { + if h.opsService == nil { + response.Error(c, http.StatusServiceUnavailable, "Ops service not available") + return + } + if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil { + response.ErrorFrom(c, err) + return + } + + var req service.OpsMetricThresholds + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request body") + return + } + + updated, err := h.opsService.UpdateMetricThresholds(c.Request.Context(), &req) + if err != nil { + response.Error(c, http.StatusBadRequest, err.Error()) + return + } + response.Success(c, updated) +} + diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go index a2f1b8c7..98d621c0 100644 --- a/backend/internal/server/routes/admin.go +++ b/backend/internal/server/routes/admin.go @@ -96,6 +96,13 @@ func registerOpsRoutes(admin *gin.RouterGroup, h *handler.Handlers) { ops.GET("/advanced-settings", h.Admin.Ops.GetAdvancedSettings) ops.PUT("/advanced-settings", h.Admin.Ops.UpdateAdvancedSettings) + // Settings group (DB-backed) + settings := ops.Group("/settings") + { + settings.GET("/metric-thresholds", h.Admin.Ops.GetMetricThresholds) + settings.PUT("/metric-thresholds", h.Admin.Ops.UpdateMetricThresholds) + } + // WebSocket realtime (QPS/TPS) ws := ops.Group("/ws") { diff --git a/backend/internal/service/ops_settings.go b/backend/internal/service/ops_settings.go index fbf8f069..3252ec20 100644 --- a/backend/internal/service/ops_settings.go +++ b/backend/internal/service/ops_settings.go @@ -463,3 +463,94 @@ func (s *OpsService) UpdateOpsAdvancedSettings(ctx context.Context, cfg *OpsAdva _ = json.Unmarshal(raw, updated) return updated, nil } + +// ========================= +// Metric thresholds +// ========================= + +const SettingKeyOpsMetricThresholds = "ops_metric_thresholds" + +func defaultOpsMetricThresholds() *OpsMetricThresholds { + slaMin := 99.5 + latencyMax := 2000.0 + ttftMax := 500.0 + reqErrMax := 5.0 + upstreamErrMax := 5.0 + return &OpsMetricThresholds{ + SLAPercentMin: &slaMin, + LatencyP99MsMax: &latencyMax, + TTFTp99MsMax: &ttftMax, + RequestErrorRatePercentMax: &reqErrMax, + UpstreamErrorRatePercentMax: &upstreamErrMax, + } +} + +func (s *OpsService) GetMetricThresholds(ctx context.Context) (*OpsMetricThresholds, error) { + defaultCfg := defaultOpsMetricThresholds() + if s == nil || s.settingRepo == nil { + return defaultCfg, nil + } + if ctx == nil { + ctx = context.Background() + } + + raw, err := s.settingRepo.GetValue(ctx, SettingKeyOpsMetricThresholds) + if err != nil { + if errors.Is(err, ErrSettingNotFound) { + if b, mErr := json.Marshal(defaultCfg); mErr == nil { + _ = s.settingRepo.Set(ctx, SettingKeyOpsMetricThresholds, string(b)) + } + return defaultCfg, nil + } + return nil, err + } + + cfg := &OpsMetricThresholds{} + if err := json.Unmarshal([]byte(raw), cfg); err != nil { + return defaultCfg, nil + } + + return cfg, nil +} + +func (s *OpsService) UpdateMetricThresholds(ctx context.Context, cfg *OpsMetricThresholds) (*OpsMetricThresholds, error) { + if s == nil || s.settingRepo == nil { + return nil, errors.New("setting repository not initialized") + } + if ctx == nil { + ctx = context.Background() + } + if cfg == nil { + return nil, errors.New("invalid config") + } + + // Validate thresholds + if cfg.SLAPercentMin != nil && (*cfg.SLAPercentMin < 0 || *cfg.SLAPercentMin > 100) { + return nil, errors.New("sla_percent_min must be between 0 and 100") + } + if cfg.LatencyP99MsMax != nil && *cfg.LatencyP99MsMax < 0 { + return nil, errors.New("latency_p99_ms_max must be >= 0") + } + if cfg.TTFTp99MsMax != nil && *cfg.TTFTp99MsMax < 0 { + return nil, errors.New("ttft_p99_ms_max must be >= 0") + } + if cfg.RequestErrorRatePercentMax != nil && (*cfg.RequestErrorRatePercentMax < 0 || *cfg.RequestErrorRatePercentMax > 100) { + return nil, errors.New("request_error_rate_percent_max must be between 0 and 100") + } + if cfg.UpstreamErrorRatePercentMax != nil && (*cfg.UpstreamErrorRatePercentMax < 0 || *cfg.UpstreamErrorRatePercentMax > 100) { + return nil, errors.New("upstream_error_rate_percent_max must be between 0 and 100") + } + + raw, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + if err := s.settingRepo.Set(ctx, SettingKeyOpsMetricThresholds, string(raw)); err != nil { + return nil, err + } + + updated := &OpsMetricThresholds{} + _ = json.Unmarshal(raw, updated) + return updated, nil +} + diff --git a/backend/internal/service/ops_settings_models.go b/backend/internal/service/ops_settings_models.go index 7d9a823c..a6fef95e 100644 --- a/backend/internal/service/ops_settings_models.go +++ b/backend/internal/service/ops_settings_models.go @@ -61,11 +61,20 @@ type OpsAlertSilencingSettings struct { Entries []OpsAlertSilenceEntry `json:"entries,omitempty"` } +type OpsMetricThresholds struct { + SLAPercentMin *float64 `json:"sla_percent_min,omitempty"` // SLA低于此值变红 + LatencyP99MsMax *float64 `json:"latency_p99_ms_max,omitempty"` // 延迟P99高于此值变红 + TTFTp99MsMax *float64 `json:"ttft_p99_ms_max,omitempty"` // TTFT P99高于此值变红 + RequestErrorRatePercentMax *float64 `json:"request_error_rate_percent_max,omitempty"` // 请求错误率高于此值变红 + UpstreamErrorRatePercentMax *float64 `json:"upstream_error_rate_percent_max,omitempty"` // 上游错误率高于此值变红 +} + type OpsAlertRuntimeSettings struct { EvaluationIntervalSeconds int `json:"evaluation_interval_seconds"` DistributedLock OpsDistributedLockSettings `json:"distributed_lock"` Silencing OpsAlertSilencingSettings `json:"silencing"` + Thresholds OpsMetricThresholds `json:"thresholds"` // 指标阈值配置 } // OpsAdvancedSettings stores advanced ops configuration (data retention, aggregation). From f28d4b78e746e61c1efaee71c9cff87fec5d5b41 Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:43:15 +0800 Subject: [PATCH 02/14] =?UTF-8?q?feat(ops):=20=E5=89=8D=E7=AB=AF=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=8C=87=E6=A0=87=E9=98=88=E5=80=BC=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E5=92=8CAPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加OpsMetricThresholds类型定义 - 新增getMetricThresholds和updateMetricThresholds API方法 --- frontend/src/api/admin/ops.ts | 24 +++++++++++++++++++++++- frontend/src/views/admin/ops/types.ts | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/frontend/src/api/admin/ops.ts b/frontend/src/api/admin/ops.ts index 1d1453f5..f52227ca 100644 --- a/frontend/src/api/admin/ops.ts +++ b/frontend/src/api/admin/ops.ts @@ -661,6 +661,14 @@ export interface EmailNotificationConfig { } } +export interface OpsMetricThresholds { + sla_percent_min?: number | null // SLA低于此值变红 + latency_p99_ms_max?: number | null // 延迟P99高于此值变红 + ttft_p99_ms_max?: number | null // TTFT P99高于此值变红 + request_error_rate_percent_max?: number | null // 请求错误率高于此值变红 + upstream_error_rate_percent_max?: number | null // 上游错误率高于此值变红 +} + export interface OpsDistributedLockSettings { enabled: boolean key: string @@ -681,6 +689,7 @@ export interface OpsAlertRuntimeSettings { reason: string }> } + thresholds: OpsMetricThresholds // 指标阈值配置 } export interface OpsAdvancedSettings { @@ -929,6 +938,17 @@ export async function updateAdvancedSettings(config: OpsAdvancedSettings): Promi return data } +// ==================== Metric Thresholds ==================== + +async function getMetricThresholds(): Promise { + const { data } = await apiClient.get('/admin/ops/settings/metric-thresholds') + return data +} + +async function updateMetricThresholds(thresholds: OpsMetricThresholds): Promise { + await apiClient.put('/admin/ops/settings/metric-thresholds', thresholds) +} + export const opsAPI = { getDashboardOverview, getThroughputTrend, @@ -952,7 +972,9 @@ export const opsAPI = { getAlertRuntimeSettings, updateAlertRuntimeSettings, getAdvancedSettings, - updateAdvancedSettings + updateAdvancedSettings, + getMetricThresholds, + updateMetricThresholds } export default opsAPI diff --git a/frontend/src/views/admin/ops/types.ts b/frontend/src/views/admin/ops/types.ts index 45ba031f..005d0427 100644 --- a/frontend/src/views/admin/ops/types.ts +++ b/frontend/src/views/admin/ops/types.ts @@ -14,6 +14,7 @@ export type { EmailNotificationConfig, OpsDistributedLockSettings, OpsAlertRuntimeSettings, + OpsMetricThresholds, OpsAdvancedSettings, OpsDataRetentionSettings, OpsAggregationSettings From bd74bf79945b575d72c4f022ca67dd24cf6605a9 Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:43:35 +0800 Subject: [PATCH 03/14] =?UTF-8?q?fix(ops):=20=E6=B7=BB=E5=8A=A0brain?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E6=9B=BF=E6=8D=A2emoji=E8=A1=A8=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在Icon组件中添加brain图标 - 用于替换运维诊断中的emoji表情 --- frontend/src/components/icons/Icon.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/icons/Icon.vue b/frontend/src/components/icons/Icon.vue index ec3c9a1b..c8ab8aed 100644 --- a/frontend/src/components/icons/Icon.vue +++ b/frontend/src/components/icons/Icon.vue @@ -124,7 +124,8 @@ const icons = { chatBubble: 'M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z', calculator: 'M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z', fire: 'M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 .5-5 2.986-7C14 5 16.09 5.777 17.656 7.343A7.975 7.975 0 0120 13a7.975 7.975 0 01-2.343 5.657z', - badge: 'M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 01-1.043 3.296 3.745 3.745 0 01-3.296 1.043A3.745 3.745 0 0112 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 01-3.296-1.043 3.745 3.745 0 01-1.043-3.296A3.745 3.745 0 013 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 011.043-3.296 3.746 3.746 0 013.296-1.043A3.746 3.746 0 0112 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 013.296 1.043 3.746 3.746 0 011.043 3.296A3.745 3.745 0 0121 12z' + badge: 'M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 01-1.043 3.296 3.745 3.745 0 01-3.296 1.043A3.745 3.745 0 0112 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 01-3.296-1.043 3.745 3.745 0 01-1.043-3.296A3.745 3.745 0 013 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 011.043-3.296 3.746 3.746 0 013.296-1.043A3.746 3.746 0 0112 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 013.296 1.043 3.746 3.746 0 011.043 3.296A3.745 3.745 0 0121 12z', + brain: 'M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15a9.065 9.065 0 00-6.23.693L5 14.5m0 0l-2.69 2.689c-1.232 1.232-.65 3.318 1.067 3.611A48.309 48.309 0 0012 21c2.773 0 5.491-.235 8.135-.687 1.718-.293 2.3-2.379 1.067-3.61L19.8 15.3M12 8.25a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm0 0v3m-3-1.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm0 0h6m-3 4.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z' } as const const iconPath = computed(() => icons[props.name]) 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 04/14] =?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') }}

+
+
+
+
From 72a2ed958b1a4e79cb20ef12a6ed7725369e9fd0 Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:44:14 +0800 Subject: [PATCH 05/14] =?UTF-8?q?feat(ops):=20=E7=9C=8B=E6=9D=BF=E4=B8=8A?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E6=8C=87=E6=A0=87=E9=98=88=E5=80=BC=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在OpsDashboard中加载阈值配置 - 在OpsDashboardHeader中根据阈值判断指标是否超标 - 超出阈值的指标显示为红色(SLA低于阈值也显示红色) - 用Icon组件替换emoji表情 --- frontend/src/views/admin/ops/OpsDashboard.vue | 25 +++++++- .../ops/components/OpsDashboardHeader.vue | 64 +++++++++++++++---- 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/frontend/src/views/admin/ops/OpsDashboard.vue b/frontend/src/views/admin/ops/OpsDashboard.vue index e8fedc5a..15f269c2 100644 --- a/frontend/src/views/admin/ops/OpsDashboard.vue +++ b/frontend/src/views/admin/ops/OpsDashboard.vue @@ -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 @@ - + @@ -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(null) +const metricThresholds = ref(null) const throughputTrend = ref(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() diff --git a/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue b/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue index ccb5dac7..d6f0025a 100644 --- a/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue +++ b/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue @@ -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" >
-

- 🧠 {{ t('admin.ops.diagnosis.title') }} +

+ + {{ t('admin.ops.diagnosis.title') }}

@@ -850,8 +889,9 @@ function openJobsDetails() {
{{ item.message }}
{{ item.impact }}
-
- 💡 {{ item.action }} +
+ + {{ item.action }}
@@ -1061,7 +1101,7 @@ function openJobsDetails() {
SLA - +
-
+
{{ slaPercent == null ? '-' : `${slaPercent.toFixed(3)}%` }}
-
+
@@ -1101,7 +1141,7 @@ function openJobsDetails() {
-
+
{{ durationP99Ms ?? '-' }}
ms (P99) @@ -1151,7 +1191,7 @@ function openJobsDetails() {
-
+
{{ ttftP99Ms ?? '-' }}
ms (P99) @@ -1196,7 +1236,7 @@ function openJobsDetails() { {{ t('admin.ops.requestDetails.details') }}
-
+
{{ errorRatePercent == null ? '-' : `${errorRatePercent.toFixed(2)}%` }}
@@ -1222,7 +1262,7 @@ function openJobsDetails() { {{ t('admin.ops.requestDetails.details') }}
-
+
{{ upstreamErrorRatePercent == null ? '-' : `${upstreamErrorRatePercent.toFixed(2)}%` }}
From db51e65b42f0377eed651803741951064e4f72e9 Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:44:34 +0800 Subject: [PATCH 06/14] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0ESLint?= =?UTF-8?q?=E5=BF=BD=E7=95=A5=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加.eslintignore文件 --- frontend/.eslintignore | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 frontend/.eslintignore diff --git a/frontend/.eslintignore b/frontend/.eslintignore new file mode 100644 index 00000000..d8682246 --- /dev/null +++ b/frontend/.eslintignore @@ -0,0 +1,14 @@ +# 忽略编译后的文件 +vite.config.js +vite.config.d.ts + +# 忽略依赖 +node_modules/ + +# 忽略构建输出 +dist/ +../backend/internal/web/dist/ + +# 忽略缓存 +.cache/ +.vite/ From f55ba3f6c1b0053890ead4e63d4bf811b4ef5e69 Mon Sep 17 00:00:00 2001 From: IanShaw027 <131567472+IanShaw027@users.noreply.github.com> Date: Mon, 12 Jan 2026 13:00:39 +0800 Subject: [PATCH 07/14] =?UTF-8?q?fix(ops):=20=E4=BC=98=E5=8C=96=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E6=A0=87=E9=A2=98=E5=92=8C=E6=98=8E=E7=BB=86=E7=AD=9B?= =?UTF-8?q?=E9=80=89=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将"请求数"改为"请求" - SLA卡片明细只显示错误请求(kind='error') - TTFT卡片明细按延迟降序排序 --- frontend/src/i18n/locales/zh.ts | 2 +- .../ops/components/OpsDashboardHeader.vue | 141 +++++++++++++----- 2 files changed, 102 insertions(+), 41 deletions(-) diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 9ccd5678..8114fb1c 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -2047,7 +2047,7 @@ export default { avg: 'avg', max: 'max', qps: 'QPS', - requests: '请求数', + requests: '请求', upstream: '上游', client: '客户端', system: '系统', diff --git a/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue b/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue index d6f0025a..8682683e 100644 --- a/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue +++ b/frontend/src/views/admin/ops/components/OpsDashboardHeader.vue @@ -1,12 +1,13 @@