fix(ops): 修复告警状态验证和错误处理逻辑
- 增强告警事件状态验证,添加合法状态值检查 - 移除重试逻辑中的遗留字段赋值 - 修正仓库不可用时的错误类型 - 格式化测试文件代码
This commit is contained in:
@@ -544,8 +544,14 @@ func (h *OpsHandler) ListAlertEvents(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cursor pagination
|
// Cursor pagination: both params must be provided together.
|
||||||
if rawTS := strings.TrimSpace(c.Query("before_fired_at")); rawTS != "" {
|
rawTS := strings.TrimSpace(c.Query("before_fired_at"))
|
||||||
|
rawID := strings.TrimSpace(c.Query("before_id"))
|
||||||
|
if (rawTS == "") != (rawID == "") {
|
||||||
|
response.BadRequest(c, "before_fired_at and before_id must be provided together")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if rawTS != "" {
|
||||||
ts, err := time.Parse(time.RFC3339Nano, rawTS)
|
ts, err := time.Parse(time.RFC3339Nano, rawTS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if t2, err2 := time.Parse(time.RFC3339, rawTS); err2 == nil {
|
if t2, err2 := time.Parse(time.RFC3339, rawTS); err2 == nil {
|
||||||
@@ -557,7 +563,7 @@ func (h *OpsHandler) ListAlertEvents(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
filter.BeforeFiredAt = &ts
|
filter.BeforeFiredAt = &ts
|
||||||
}
|
}
|
||||||
if rawID := strings.TrimSpace(c.Query("before_id")); rawID != "" {
|
if rawID != "" {
|
||||||
id, err := strconv.ParseInt(rawID, 10, 64)
|
id, err := strconv.ParseInt(rawID, 10, 64)
|
||||||
if err != nil || id <= 0 {
|
if err != nil || id <= 0 {
|
||||||
response.BadRequest(c, "Invalid before_id")
|
response.BadRequest(c, "Invalid before_id")
|
||||||
|
|||||||
@@ -925,9 +925,13 @@ func buildOpsErrorLogsWhere(filter *service.OpsErrorLogFilter) (string, []any) {
|
|||||||
// ops_error_logs stores client-visible error requests (status>=400),
|
// ops_error_logs stores client-visible error requests (status>=400),
|
||||||
// but we also persist "recovered" upstream errors (status<400) for upstream health visibility.
|
// but we also persist "recovered" upstream errors (status<400) for upstream health visibility.
|
||||||
// By default, keep list endpoints scoped to unresolved records if the caller didn't specify.
|
// By default, keep list endpoints scoped to unresolved records if the caller didn't specify.
|
||||||
if filter != nil && filter.Resolved == nil {
|
resolvedFilter := (*bool)(nil)
|
||||||
|
if filter != nil {
|
||||||
|
resolvedFilter = filter.Resolved
|
||||||
|
}
|
||||||
|
if resolvedFilter == nil {
|
||||||
f := false
|
f := false
|
||||||
filter.Resolved = &f
|
resolvedFilter = &f
|
||||||
}
|
}
|
||||||
// Keep list endpoints scoped to client errors unless explicitly filtering upstream phase.
|
// Keep list endpoints scoped to client errors unless explicitly filtering upstream phase.
|
||||||
if phaseFilter != "upstream" {
|
if phaseFilter != "upstream" {
|
||||||
@@ -967,8 +971,8 @@ func buildOpsErrorLogsWhere(filter *service.OpsErrorLogFilter) (string, []any) {
|
|||||||
args = append(args, source)
|
args = append(args, source)
|
||||||
clauses = append(clauses, "LOWER(COALESCE(error_source,'')) = $"+itoa(len(args)))
|
clauses = append(clauses, "LOWER(COALESCE(error_source,'')) = $"+itoa(len(args)))
|
||||||
}
|
}
|
||||||
if filter.Resolved != nil {
|
if resolvedFilter != nil {
|
||||||
args = append(args, *filter.Resolved)
|
args = append(args, *resolvedFilter)
|
||||||
clauses = append(clauses, "COALESCE(resolved,false) = $"+itoa(len(args)))
|
clauses = append(clauses, "COALESCE(resolved,false) = $"+itoa(len(args)))
|
||||||
}
|
}
|
||||||
if len(filter.StatusCodes) > 0 {
|
if len(filter.StatusCodes) > 0 {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func TestSchedulerSnapshotOutboxReplay(t *testing.T) {
|
|||||||
RunMode: config.RunModeStandard,
|
RunMode: config.RunModeStandard,
|
||||||
Gateway: config.GatewayConfig{
|
Gateway: config.GatewayConfig{
|
||||||
Scheduling: config.GatewaySchedulingConfig{
|
Scheduling: config.GatewaySchedulingConfig{
|
||||||
OutboxPollIntervalSeconds: 1,
|
OutboxPollIntervalSeconds: 1,
|
||||||
FullRebuildIntervalSeconds: 0,
|
FullRebuildIntervalSeconds: 0,
|
||||||
DbFallbackEnabled: true,
|
DbFallbackEnabled: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -416,8 +416,8 @@ func (s *UsageLogRepoSuite) TestDashboardAggregationConsistency() {
|
|||||||
// 使用固定的时间偏移确保 hour1 和 hour2 在同一天且都在过去
|
// 使用固定的时间偏移确保 hour1 和 hour2 在同一天且都在过去
|
||||||
// 选择当天 02:00 和 03:00 作为测试时间点(基于 now 的日期)
|
// 选择当天 02:00 和 03:00 作为测试时间点(基于 now 的日期)
|
||||||
dayStart := truncateToDayUTC(now)
|
dayStart := truncateToDayUTC(now)
|
||||||
hour1 := dayStart.Add(2 * time.Hour) // 当天 02:00
|
hour1 := dayStart.Add(2 * time.Hour) // 当天 02:00
|
||||||
hour2 := dayStart.Add(3 * time.Hour) // 当天 03:00
|
hour2 := dayStart.Add(3 * time.Hour) // 当天 03:00
|
||||||
// 如果当前时间早于 hour2,则使用昨天的时间
|
// 如果当前时间早于 hour2,则使用昨天的时间
|
||||||
if now.Before(hour2.Add(time.Hour)) {
|
if now.Before(hour2.Add(time.Hour)) {
|
||||||
dayStart = dayStart.Add(-24 * time.Hour)
|
dayStart = dayStart.Add(-24 * time.Hour)
|
||||||
|
|||||||
@@ -262,11 +262,11 @@ func TestAPIContracts(t *testing.T) {
|
|||||||
name: "GET /api/v1/admin/settings",
|
name: "GET /api/v1/admin/settings",
|
||||||
setup: func(t *testing.T, deps *contractDeps) {
|
setup: func(t *testing.T, deps *contractDeps) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
deps.settingRepo.SetAll(map[string]string{
|
deps.settingRepo.SetAll(map[string]string{
|
||||||
service.SettingKeyRegistrationEnabled: "true",
|
service.SettingKeyRegistrationEnabled: "true",
|
||||||
service.SettingKeyEmailVerifyEnabled: "false",
|
service.SettingKeyEmailVerifyEnabled: "false",
|
||||||
|
|
||||||
service.SettingKeySMTPHost: "smtp.example.com",
|
service.SettingKeySMTPHost: "smtp.example.com",
|
||||||
service.SettingKeySMTPPort: "587",
|
service.SettingKeySMTPPort: "587",
|
||||||
service.SettingKeySMTPUsername: "user",
|
service.SettingKeySMTPUsername: "user",
|
||||||
service.SettingKeySMTPPassword: "secret",
|
service.SettingKeySMTPPassword: "secret",
|
||||||
@@ -285,15 +285,15 @@ func TestAPIContracts(t *testing.T) {
|
|||||||
service.SettingKeyContactInfo: "support",
|
service.SettingKeyContactInfo: "support",
|
||||||
service.SettingKeyDocURL: "https://docs.example.com",
|
service.SettingKeyDocURL: "https://docs.example.com",
|
||||||
|
|
||||||
service.SettingKeyDefaultConcurrency: "5",
|
service.SettingKeyDefaultConcurrency: "5",
|
||||||
service.SettingKeyDefaultBalance: "1.25",
|
service.SettingKeyDefaultBalance: "1.25",
|
||||||
|
|
||||||
service.SettingKeyOpsMonitoringEnabled: "false",
|
service.SettingKeyOpsMonitoringEnabled: "false",
|
||||||
service.SettingKeyOpsRealtimeMonitoringEnabled: "true",
|
service.SettingKeyOpsRealtimeMonitoringEnabled: "true",
|
||||||
service.SettingKeyOpsQueryModeDefault: "auto",
|
service.SettingKeyOpsQueryModeDefault: "auto",
|
||||||
service.SettingKeyOpsMetricsIntervalSeconds: "60",
|
service.SettingKeyOpsMetricsIntervalSeconds: "60",
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: "/api/v1/admin/settings",
|
path: "/api/v1/admin/settings",
|
||||||
wantStatus: http.StatusOK,
|
wantStatus: http.StatusOK,
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
|
|
||||||
type accountRepoStubForBulkUpdate struct {
|
type accountRepoStubForBulkUpdate struct {
|
||||||
accountRepoStub
|
accountRepoStub
|
||||||
bulkUpdateErr error
|
bulkUpdateErr error
|
||||||
bulkUpdateIDs []int64
|
bulkUpdateIDs []int64
|
||||||
bindGroupErrByID map[int64]error
|
bindGroupErrByID map[int64]error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *accountRepoStubForBulkUpdate) BulkUpdate(_ context.Context, ids []int64, _ AccountBulkUpdate) (int64, error) {
|
func (s *accountRepoStubForBulkUpdate) BulkUpdate(_ context.Context, ids []int64, _ AccountBulkUpdate) (int64, error) {
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ func (s *OpsAlertEvaluatorService) evaluateOnce(interval time.Duration) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
scopePlatform, scopeGroupID := parseOpsAlertRuleScope(rule.Filters)
|
scopePlatform, scopeGroupID, scopeRegion := parseOpsAlertRuleScope(rule.Filters)
|
||||||
|
|
||||||
windowMinutes := rule.WindowMinutes
|
windowMinutes := rule.WindowMinutes
|
||||||
if windowMinutes <= 0 {
|
if windowMinutes <= 0 {
|
||||||
@@ -239,7 +239,7 @@ func (s *OpsAlertEvaluatorService) evaluateOnce(interval time.Duration) {
|
|||||||
// Scoped silencing: if a matching silence exists, skip creating a firing event.
|
// Scoped silencing: if a matching silence exists, skip creating a firing event.
|
||||||
if s.opsService != nil {
|
if s.opsService != nil {
|
||||||
platform := strings.TrimSpace(scopePlatform)
|
platform := strings.TrimSpace(scopePlatform)
|
||||||
region := (*string)(nil)
|
region := scopeRegion
|
||||||
if platform != "" {
|
if platform != "" {
|
||||||
if ok, err := s.opsService.IsAlertSilenced(ctx, rule.ID, platform, scopeGroupID, region, now); err == nil && ok {
|
if ok, err := s.opsService.IsAlertSilenced(ctx, rule.ID, platform, scopeGroupID, region, now); err == nil && ok {
|
||||||
continue
|
continue
|
||||||
@@ -370,9 +370,9 @@ func requiredSustainedBreaches(sustainedMinutes int, interval time.Duration) int
|
|||||||
return required
|
return required
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOpsAlertRuleScope(filters map[string]any) (platform string, groupID *int64) {
|
func parseOpsAlertRuleScope(filters map[string]any) (platform string, groupID *int64, region *string) {
|
||||||
if filters == nil {
|
if filters == nil {
|
||||||
return "", nil
|
return "", nil, nil
|
||||||
}
|
}
|
||||||
if v, ok := filters["platform"]; ok {
|
if v, ok := filters["platform"]; ok {
|
||||||
if s, ok := v.(string); ok {
|
if s, ok := v.(string); ok {
|
||||||
@@ -403,7 +403,15 @@ func parseOpsAlertRuleScope(filters map[string]any) (platform string, groupID *i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return platform, groupID
|
if v, ok := filters["region"]; ok {
|
||||||
|
if s, ok := v.(string); ok {
|
||||||
|
vv := strings.TrimSpace(s)
|
||||||
|
if vv != "" {
|
||||||
|
region = &vv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return platform, groupID, region
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OpsAlertEvaluatorService) computeRuleMetric(
|
func (s *OpsAlertEvaluatorService) computeRuleMetric(
|
||||||
|
|||||||
@@ -208,7 +208,11 @@ func (s *OpsService) UpdateAlertEventStatus(ctx context.Context, eventID int64,
|
|||||||
if eventID <= 0 {
|
if eventID <= 0 {
|
||||||
return infraerrors.BadRequest("INVALID_EVENT_ID", "invalid event id")
|
return infraerrors.BadRequest("INVALID_EVENT_ID", "invalid event id")
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(status) == "" {
|
status = strings.TrimSpace(status)
|
||||||
|
if status == "" {
|
||||||
|
return infraerrors.BadRequest("INVALID_STATUS", "invalid status")
|
||||||
|
}
|
||||||
|
if status != OpsAlertStatusResolved && status != OpsAlertStatusManualResolved {
|
||||||
return infraerrors.BadRequest("INVALID_STATUS", "invalid status")
|
return infraerrors.BadRequest("INVALID_STATUS", "invalid status")
|
||||||
}
|
}
|
||||||
return s.opsRepo.UpdateAlertEventStatus(ctx, eventID, status, resolvedAt)
|
return s.opsRepo.UpdateAlertEventStatus(ctx, eventID, status, resolvedAt)
|
||||||
|
|||||||
@@ -220,11 +220,8 @@ func (s *OpsService) RetryError(ctx context.Context, requestedByUserID int64, er
|
|||||||
msg := result.ErrorMessage
|
msg := result.ErrorMessage
|
||||||
updateErrMsg = &msg
|
updateErrMsg = &msg
|
||||||
}
|
}
|
||||||
|
// Keep legacy result_request_id empty; use upstream_request_id instead.
|
||||||
var resultRequestID *string
|
var resultRequestID *string
|
||||||
if strings.TrimSpace(result.UpstreamRequestID) != "" {
|
|
||||||
v := result.UpstreamRequestID
|
|
||||||
resultRequestID = &v
|
|
||||||
}
|
|
||||||
|
|
||||||
finalStatus := result.Status
|
finalStatus := result.Status
|
||||||
if strings.TrimSpace(finalStatus) == "" {
|
if strings.TrimSpace(finalStatus) == "" {
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ func (s *OpsService) ListRetryAttemptsByErrorID(ctx context.Context, errorID int
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if s.opsRepo == nil {
|
if s.opsRepo == nil {
|
||||||
return nil, infraerrors.NotFound("OPS_ERROR_NOT_FOUND", "ops error log not found")
|
return nil, infraerrors.ServiceUnavailable("OPS_REPO_UNAVAILABLE", "Ops repository not available")
|
||||||
}
|
}
|
||||||
if errorID <= 0 {
|
if errorID <= 0 {
|
||||||
return nil, infraerrors.BadRequest("OPS_ERROR_INVALID_ID", "invalid error id")
|
return nil, infraerrors.BadRequest("OPS_ERROR_INVALID_ID", "invalid error id")
|
||||||
|
|||||||
@@ -150,12 +150,13 @@ export default {
|
|||||||
invalidEmail: 'Please enter a valid email address',
|
invalidEmail: 'Please enter a valid email address',
|
||||||
optional: 'optional',
|
optional: 'optional',
|
||||||
selectOption: 'Select an option',
|
selectOption: 'Select an option',
|
||||||
searchPlaceholder: 'Search...',
|
searchPlaceholder: 'Search...',
|
||||||
noOptionsFound: 'No options found',
|
noOptionsFound: 'No options found',
|
||||||
noGroupsAvailable: 'No groups available',
|
noGroupsAvailable: 'No groups available',
|
||||||
unknownError: 'Unknown error occurred',
|
unknownError: 'Unknown error occurred',
|
||||||
saving: 'Saving...',
|
saving: 'Saving...',
|
||||||
selectedCount: '({count} selected)', refresh: 'Refresh',
|
selectedCount: '({count} selected)',
|
||||||
|
refresh: 'Refresh',
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
notAvailable: 'N/A',
|
notAvailable: 'N/A',
|
||||||
now: 'Now',
|
now: 'Now',
|
||||||
|
|||||||
Reference in New Issue
Block a user