From 6cf9465feaf85f1d4ccb61e1f6545959db9028c9 Mon Sep 17 00:00:00 2001 From: Wang Defa <1+wangdefa@noreply.gitea.bcde.io> Date: Fri, 17 Jul 2026 16:52:49 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=91=E7=AB=AF=E4=BA=8B=E4=BB=B6:=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=AE=9E=E4=BE=8B=E9=80=9A=E7=9F=A5=E5=B9=B6=E7=A7=BB?= =?UTF-8?q?=E5=87=BA=20LaunchInstance=20=E5=9B=9E=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/oci/logrelay.go | 47 +++++++++++++++++++++++++++++-- internal/oci/logrelay_test.go | 30 ++++++++++++++++++++ internal/service/logevent.go | 16 +++++++++++ internal/service/logevent_test.go | 8 ++++++ internal/service/logrelay.go | 25 ++++++++++------ internal/service/logrelay_test.go | 18 ++++++++++-- 6 files changed, 131 insertions(+), 13 deletions(-) diff --git a/internal/oci/logrelay.go b/internal/oci/logrelay.go index 0cfc997..68350eb 100644 --- a/internal/oci/logrelay.go +++ b/internal/oci/logrelay.go @@ -234,8 +234,12 @@ func (c *RealClient) EnsureRelayConnector(ctx context.Context, cred Credentials, if err != nil { return RelayResource{}, err } - if res, ok, err := findRelayConnector(ctx, sc, cred.TenancyOCID); err != nil || ok { - return res, err + res, ok, err := findRelayConnector(ctx, sc, cred.TenancyOCID) + if err != nil { + return RelayResource{}, err + } + if ok { + return res, reconcileRelayCondition(ctx, sc, res.ID, condition) } details := sch.CreateServiceConnectorDetails{ DisplayName: common.String(relayConnectorName), @@ -257,6 +261,45 @@ func (c *RealClient) EnsureRelayConnector(ctx context.Context, cred Credentials, return waitRelayConnector(ctx, sc, cred.TenancyOCID) } +// reconcileRelayCondition 对齐存量 Connector 的过滤条件:关键事件清单变更 +// (如去除 LaunchInstance)后,已建链路经「一键创建」幂等调用原地更新,无需拆除重建。 +func reconcileRelayCondition(ctx context.Context, sc sch.ServiceConnectorClient, id, condition string) error { + got, err := sc.GetServiceConnector(ctx, sch.GetServiceConnectorRequest{ServiceConnectorId: &id}) + if err != nil { + return fmt.Errorf("get service connector: %w", err) + } + if !relayConditionDiffers(got.Tasks, condition) { + return nil + } + details := sch.UpdateServiceConnectorDetails{Tasks: []sch.TaskDetails{}} + if condition != "" { + details.Tasks = []sch.TaskDetails{sch.LogRuleTaskDetails{Condition: &condition}} + } + _, err = sc.UpdateServiceConnector(ctx, sch.UpdateServiceConnectorRequest{ + ServiceConnectorId: &id, UpdateServiceConnectorDetails: details, + }) + if err != nil { + return fmt.Errorf("update service connector condition: %w", err) + } + return nil +} + +// relayConditionDiffers 判断现有任务集与期望条件是否不一致: +// 期望形态是单条 LogRule 条件;任务数、类型或条件文本不同均视为漂移。 +func relayConditionDiffers(tasks []sch.TaskDetailsResponse, want string) bool { + if want == "" { + return len(tasks) > 0 + } + if len(tasks) != 1 { + return true + } + rule, ok := tasks[0].(sch.LogRuleTaskDetailsResponse) + if !ok || rule.Condition == nil { + return true + } + return *rule.Condition != want +} + // findRelayConnector 按名查找存活 Connector。 func findRelayConnector(ctx context.Context, sc sch.ServiceConnectorClient, tenancy string) (RelayResource, bool, error) { list, err := sc.ListServiceConnectors(ctx, sch.ListServiceConnectorsRequest{ diff --git a/internal/oci/logrelay_test.go b/internal/oci/logrelay_test.go index cd11a40..891d203 100644 --- a/internal/oci/logrelay_test.go +++ b/internal/oci/logrelay_test.go @@ -3,6 +3,8 @@ package oci import ( "strings" "testing" + + "github.com/oracle/oci-go-sdk/v65/sch" ) func TestRelayEventCondition(t *testing.T) { @@ -35,3 +37,31 @@ func TestRelayEventConditionQuoting(t *testing.T) { t.Errorf("单引号数 = %d, want 2 (%s)", strings.Count(cond, "'"), cond) } } + +func TestRelayConditionDiffers(t *testing.T) { + cond := "data.eventName='TerminateInstance'" + rule := func(c string) sch.TaskDetailsResponse { + return sch.LogRuleTaskDetailsResponse{Condition: &c} + } + tests := []struct { + name string + tasks []sch.TaskDetailsResponse + want string + diff bool + }{ + {name: "条件一致不漂移", tasks: []sch.TaskDetailsResponse{rule(cond)}, want: cond, diff: false}, + {name: "条件文本不同漂移", tasks: []sch.TaskDetailsResponse{rule("data.eventName='LaunchInstance'")}, want: cond, diff: true}, + {name: "无任务但期望条件漂移", tasks: nil, want: cond, diff: true}, + {name: "多任务漂移", tasks: []sch.TaskDetailsResponse{rule(cond), rule(cond)}, want: cond, diff: true}, + {name: "期望空且无任务不漂移", tasks: nil, want: "", diff: false}, + {name: "期望空但有任务漂移", tasks: []sch.TaskDetailsResponse{rule(cond)}, want: "", diff: true}, + {name: "条件缺失漂移", tasks: []sch.TaskDetailsResponse{sch.LogRuleTaskDetailsResponse{}}, want: cond, diff: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := relayConditionDiffers(tt.tasks, tt.want); got != tt.diff { + t.Errorf("differs = %v, want %v", got, tt.diff) + } + }) + } +} diff --git a/internal/service/logevent.go b/internal/service/logevent.go index 2254aab..48cde56 100644 --- a/internal/service/logevent.go +++ b/internal/service/logevent.go @@ -554,6 +554,8 @@ func envActor(env onsEnvelope) string { // envOutcome 判读事件成败与补充说明:登录事件解析 auditEventMapValue;其余 // Audit 事件看 message 后缀,补充说明取 stateChange.current.description(策略描述)。 +// 计算类失败形如 "LaunchInstance failed with response 'NotAuthorizedOrNotFound'", +// 判失败并以引号内错误码作补充说明(无其他说明时)。 func envOutcome(env onsEnvelope) (outcome, detail string) { if env.StateChange != nil { detail = env.StateChange.Current.Description @@ -566,10 +568,24 @@ func envOutcome(env onsEnvelope) (outcome, detail string) { outcome = "成功" case strings.HasSuffix(env.Message, " failed"): outcome = "失败" + case strings.Contains(env.Message, " failed with response "): + outcome = "失败" + if detail == "" { + detail = failedResponse(env.Message) + } } return outcome, detail } +// failedResponse 提取 "X failed with response 'Err'" 中引号内的错误码。 +func failedResponse(msg string) string { + _, after, ok := strings.Cut(msg, " failed with response ") + if !ok { + return "" + } + return strings.Trim(strings.TrimSpace(after), "'") +} + // ssoOutcome 解析 IDCS 审计负载(JSON 字符串):eventId 含 success/failure 定成败, // 失败时以其 message 作为原因说明。 func ssoOutcome(raw, detail string) (string, string) { diff --git a/internal/service/logevent_test.go b/internal/service/logevent_test.go index 3737f7f..5b14300 100644 --- a/internal/service/logevent_test.go +++ b/internal/service/logevent_test.go @@ -308,6 +308,14 @@ func TestParseLogEvent(t *testing.T) { wantType: "x", wantOutcome: "失败", }, + { + name: "failed with response 判失败并提取错误码", + payload: `{"type":"com.oraclecloud.computeApi.LaunchInstance.begin","data":{ + "message":"LaunchInstance failed with response 'NotAuthorizedOrNotFound'"}}`, + wantType: "com.oraclecloud.computeApi.LaunchInstance.begin", + wantOutcome: "失败", + wantDetail: "NotAuthorizedOrNotFound", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/service/logrelay.go b/internal/service/logrelay.go index 4b3939e..a01492c 100644 --- a/internal/service/logrelay.go +++ b/internal/service/logrelay.go @@ -1,6 +1,7 @@ package service import ( + "cmp" "context" "errors" "fmt" @@ -23,9 +24,11 @@ const ( ) // RelayCriticalEvents 是回传的关键事件清单(Connector Log Filter 与 P2 告警共用): -// 实例生命周期、用户与凭据、区域订阅、策略变更、控制台登录。 +// 实例终止与电源操作、用户与凭据、区域订阅、策略变更、控制台登录。 +// 不含 LaunchInstance:创建以面板自身(手动/抢机反复尝试)发起为主,回传即噪声 +// (10 个 1min 抢机任务一天即可刷掉 2 万条上限),终止与电源操作才是外部风险信号。 var RelayCriticalEvents = []string{ - "LaunchInstance", "TerminateInstance", "InstanceAction", + "TerminateInstance", "InstanceAction", "CreateUser", "DeleteUser", "UpdateUser", "CreateApiKey", "DeleteApiKey", "UpdateUserCapabilities", "CreateRegionSubscription", @@ -46,7 +49,7 @@ var relayCriticalSet = func() map[string]bool { // 通知管理「云端事件」按此细分开关。 func relayEventClass(name string) string { switch name { - case "LaunchInstance", "TerminateInstance", "InstanceAction": + case "TerminateInstance", "InstanceAction": return "instance" case "CreateUser", "DeleteUser", "UpdateUser", "CreateApiKey", "DeleteApiKey", "UpdateUserCapabilities": @@ -313,24 +316,30 @@ func (s *LogEventService) teardownResources(ctx context.Context, cred oci.Creden // ---- P2 告警联动:解析出关键事件后经 Notifier 推送 ---- -// relayEventShortName 取 CloudEvents type 末段(com.oraclecloud.ComputeApi.LaunchInstance → LaunchInstance)。 +// relayEventShortName 取 CloudEvents type 中的事件短名:Audit v2 计算类事件带 +// .begin/.end 阶段后缀(com.oraclecloud.ComputeApi.LaunchInstance.end),先剥阶段再取末段。 func relayEventShortName(eventType string) string { - if i := strings.LastIndex(eventType, "."); i >= 0 { - return eventType[i+1:] + t := strings.TrimSuffix(strings.TrimSuffix(eventType, ".begin"), ".end") + if i := strings.LastIndex(t, "."); i >= 0 { + return t[i+1:] } - return eventType + return t } // criticalEventVars 判定关键事件并生成模板变量;非关键事件 ok 为 false。 +// 成对事件只推 .begin(携带操作者/IP/成败),.end 无操作者且信息重复,不再告警; // actor/ip/outcome/resource 缺失时兜底 —,detail 包装为独立行(空则不占行)。 func criticalEventVars(alias string, p parsedEvent) (map[string]string, bool) { + if strings.HasSuffix(p.EventType, ".end") { + return nil, false + } name := relayEventShortName(p.EventType) if name == "" || !relayCriticalSet[name] { return nil, false } return map[string]string{ "tenant": alias, "event": name, - "resource": orDash(p.ResourceName), "actor": orDash(p.Actor), + "resource": orDash(cmp.Or(p.ResourceName, p.Source)), "actor": orDash(p.Actor), "ip": orDash(p.SourceIP), "outcome": orDash(p.Outcome), "detail": detailLine(p.Detail), }, true diff --git a/internal/service/logrelay_test.go b/internal/service/logrelay_test.go index 6fa15ab..d9e71f0 100644 --- a/internal/service/logrelay_test.go +++ b/internal/service/logrelay_test.go @@ -339,10 +339,22 @@ func TestCriticalEventText(t *testing.T) { wantOK: true, want: map[string]string{"event": "CreatePolicy", "detail": "\n允许发布到 ONS Topic"}, }, + { + name: "begin 阶段剥后缀命中并回退 source 作资源", + event: parsedEvent{EventType: "com.oraclecloud.computeApi.TerminateInstance.begin", + Source: "instance-20260717-1445", Actor: "IT Team", SourceIP: "137.131.7.136", Outcome: "成功"}, + wantOK: true, + want: map[string]string{"event": "TerminateInstance", "resource": "instance-20260717-1445", + "actor": "IT Team", "ip": "137.131.7.136", "outcome": "成功"}, + }, + {name: "end 阶段不重复告警", + event: parsedEvent{EventType: "com.oraclecloud.ComputeApi.TerminateInstance.end"}, wantOK: false}, + {name: "LaunchInstance 已移出清单不告警", + event: parsedEvent{EventType: "com.oraclecloud.computeApi.LaunchInstance.begin"}, wantOK: false}, {name: "List 噪声不推", event: parsedEvent{EventType: "com.oraclecloud.ComputeApi.ListInstances"}, wantOK: false}, {name: "空类型不推", event: parsedEvent{}, wantOK: false}, - {name: "短名直接命中", event: parsedEvent{EventType: "LaunchInstance"}, wantOK: true, - want: map[string]string{"event": "LaunchInstance"}}, + {name: "短名直接命中", event: parsedEvent{EventType: "InstanceAction"}, wantOK: true, + want: map[string]string{"event": "InstanceAction"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -370,7 +382,7 @@ func TestRelayEventClass(t *testing.T) { name string class string }{ - {"LaunchInstance", "instance"}, + {"LaunchInstance", ""}, // 已移出回传清单,不归类 {"TerminateInstance", "instance"}, {"InstanceAction", "instance"}, {"CreateUser", "identity"},