feat(ops): 新增 account_temp_unscheduled_count 告警指标

临时摘除(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) <noreply@anthropic.com>
This commit is contained in:
DaydreamCoding
2026-06-07 22:31:45 +08:00
co-authored by Claude Opus 4.8
parent 217f85999c
commit f20e6bf769
7 changed files with 68 additions and 0 deletions
@@ -29,6 +29,7 @@ var validOpsAlertMetricTypes = []string{
"account_rate_limited_count",
"account_error_count",
"account_error_ratio",
"account_temp_unscheduled_count",
"overload_account_count",
}
@@ -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
@@ -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()
+1
View File
@@ -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 = '>' | '>=' | '<' | '<=' | '==' | '!='
+2
View File
@@ -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: {
+2
View File
@@ -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: {
@@ -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',