From f20e6bf769221633a7ee6d8fec9e0b80a981628d Mon Sep 17 00:00:00 2001 From: DaydreamCoding Date: Mon, 1 Jun 2026 14:47:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(ops):=20=E6=96=B0=E5=A2=9E=20account=5Ftem?= =?UTF-8?q?p=5Funscheduled=5Fcount=20=E5=91=8A=E8=AD=A6=E6=8C=87=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 临时摘除(temp-unschedulable)的账号被 account_error_count 指标显式排除 (acc.HasError && TempUnschedulableUntil == nil),且 SetTempUnschedulable 不 改账号 Status,导致代理/凭据故障触发的自动摘除无法被现有告警覆盖。 新增 account_temp_unscheduled_count 指标,统计当前处于临时不可调度窗口 (TempUnschedulableUntil 未过期) 的账号数,打通对自动摘除的定向告警: - evaluator computeRuleMetric 新增分支 + handler 允许列表; - 前端联合类型、告警规则下拉项与 en/zh 文案同步。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../handler/admin/ops_alerts_handler.go | 1 + .../service/ops_alert_evaluator_service.go | 12 ++++++ .../ops_alert_evaluator_service_test.go | 42 +++++++++++++++++++ frontend/src/api/admin/ops.ts | 1 + frontend/src/i18n/locales/en.ts | 2 + frontend/src/i18n/locales/zh.ts | 2 + .../ops/components/OpsAlertRulesCard.vue | 8 ++++ 7 files changed, 68 insertions(+) diff --git a/backend/internal/handler/admin/ops_alerts_handler.go b/backend/internal/handler/admin/ops_alerts_handler.go index edc8c7f752..6356a0bbea 100644 --- a/backend/internal/handler/admin/ops_alerts_handler.go +++ b/backend/internal/handler/admin/ops_alerts_handler.go @@ -29,6 +29,7 @@ var validOpsAlertMetricTypes = []string{ "account_rate_limited_count", "account_error_count", "account_error_ratio", + "account_temp_unscheduled_count", "overload_account_count", } diff --git a/backend/internal/service/ops_alert_evaluator_service.go b/backend/internal/service/ops_alert_evaluator_service.go index c6a58a1b59..4d3652766a 100644 --- a/backend/internal/service/ops_alert_evaluator_service.go +++ b/backend/internal/service/ops_alert_evaluator_service.go @@ -506,6 +506,18 @@ func (s *OpsAlertEvaluatorService) computeRuleMetric( return float64(countAccountsByCondition(availability.Accounts, func(acc *AccountAvailability) bool { return acc.HasError && acc.TempUnschedulableUntil == nil })), true + case "account_temp_unscheduled_count": + if s == nil || s.opsService == nil { + return 0, false + } + availability, err := s.opsService.GetAccountAvailability(ctx, platform, groupID) + if err != nil || availability == nil { + return 0, false + } + now := time.Now().UTC() + return float64(countAccountsByCondition(availability.Accounts, func(acc *AccountAvailability) bool { + return acc.TempUnschedulableUntil != nil && now.Before(*acc.TempUnschedulableUntil) + })), true case "group_rate_limit_ratio": if groupID == nil || *groupID <= 0 { return 0, false diff --git a/backend/internal/service/ops_alert_evaluator_service_test.go b/backend/internal/service/ops_alert_evaluator_service_test.go index 83d358a3a0..6ae0f1d105 100644 --- a/backend/internal/service/ops_alert_evaluator_service_test.go +++ b/backend/internal/service/ops_alert_evaluator_service_test.go @@ -106,6 +106,48 @@ func TestCountAccountsByCondition(t *testing.T) { }) } +// TestComputeRuleMetric_AccountTempUnscheduledCount verifies the new +// account_temp_unscheduled_count metric counts accounts currently in the +// temp-unscheduled window and ignores those whose window has expired or +// were never temp-unscheduled. +func TestComputeRuleMetric_AccountTempUnscheduledCount(t *testing.T) { + t.Parallel() + + now := time.Now().UTC() + futureUntil := now.Add(5 * time.Minute) + pastUntil := now.Add(-1 * time.Minute) + + availability := &OpsAccountAvailability{ + Accounts: map[int64]*AccountAvailability{ + // currently temp-unscheduled (window active) + 1: {TempUnschedulableUntil: &futureUntil}, + 2: {TempUnschedulableUntil: &futureUntil}, + // temp-unsched window already expired → should NOT count + 3: {TempUnschedulableUntil: &pastUntil}, + // never temp-unscheduled + 4: {HasError: true}, + 5: {IsRateLimited: true}, + }, + } + + opsService := &OpsService{ + getAccountAvailability: func(_ context.Context, _ string, _ *int64) (*OpsAccountAvailability, error) { + return availability, nil + }, + } + svc := &OpsAlertEvaluatorService{ + opsService: opsService, + opsRepo: &stubOpsRepo{}, + } + + rule := &OpsAlertRule{MetricType: "account_temp_unscheduled_count"} + val, ok := svc.computeRuleMetric(context.Background(), rule, nil, + now.Add(-5*time.Minute), now, "", nil) + + require.True(t, ok) + require.InDelta(t, 2.0, val, 0.0001, "only 2 accounts have an active temp-unsched window") +} + func TestComputeRuleMetricNewIndicators(t *testing.T) { t.Parallel() diff --git a/frontend/src/api/admin/ops.ts b/frontend/src/api/admin/ops.ts index a3a47c2c03..dd39426b80 100644 --- a/frontend/src/api/admin/ops.ts +++ b/frontend/src/api/admin/ops.ts @@ -685,6 +685,7 @@ export type MetricType = | 'account_rate_limited_count' | 'account_error_count' | 'account_error_ratio' + | 'account_temp_unscheduled_count' | 'overload_account_count' export type Operator = '>' | '>=' | '<' | '<=' | '==' | '!=' diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 1ad34b0e65..d58b999005 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -5028,6 +5028,7 @@ export default { accountRateLimitedCount: 'Rate-limited Accounts', accountErrorCount: 'Error Accounts (excluding temporarily unschedulable)', accountErrorRatio: 'Error Account Ratio (%)', + accountTempUnscheduledCount: 'Temporarily Unschedulable Accounts', overloadAccountCount: 'Overloaded Accounts' }, metricDescriptions: { @@ -5045,6 +5046,7 @@ export default { accountRateLimitedCount: 'Number of rate-limited accounts within the window.', accountErrorCount: 'Number of error accounts within the window (excluding temporarily unschedulable).', accountErrorRatio: 'Error account ratio within the window (0-100).', + accountTempUnscheduledCount: 'Number of accounts currently temporarily unschedulable (e.g. proxy/credential failure auto-eviction).', overloadAccountCount: 'Number of overloaded accounts within the window.' }, hints: { diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 2868e5d598..ea4339c3cb 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -5187,6 +5187,7 @@ export default { accountRateLimitedCount: '限流账号数', accountErrorCount: '错误账号数(不含临时不可调度)', accountErrorRatio: '错误账号比例 (%)', + accountTempUnscheduledCount: '临时不可调度账号数', overloadAccountCount: '过载账号数' }, metricDescriptions: { @@ -5204,6 +5205,7 @@ export default { accountRateLimitedCount: '统计窗口内被限流的账号数量。', accountErrorCount: '统计窗口内产生错误的账号数量(不含临时不可调度)。', accountErrorRatio: '统计窗口内错误账号占比(0~100)。', + accountTempUnscheduledCount: '当前处于临时不可调度状态的账号数量(如代理/凭据故障被自动摘除)。', overloadAccountCount: '统计窗口内过载账号数量。' }, hints: { diff --git a/frontend/src/views/admin/ops/components/OpsAlertRulesCard.vue b/frontend/src/views/admin/ops/components/OpsAlertRulesCard.vue index 627303c0d9..d1aecf074d 100644 --- a/frontend/src/views/admin/ops/components/OpsAlertRulesCard.vue +++ b/frontend/src/views/admin/ops/components/OpsAlertRulesCard.vue @@ -221,6 +221,14 @@ const metricDefinitions = computed(() => { recommendedThreshold: 5, unit: '%' }, + { + type: 'account_temp_unscheduled_count', + group: 'account', + label: t('admin.ops.alertRules.metrics.accountTempUnscheduledCount'), + description: t('admin.ops.alertRules.metricDescriptions.accountTempUnscheduledCount'), + recommendedOperator: '>', + recommendedThreshold: 0 + }, { type: 'overload_account_count', group: 'account',