diff --git a/backend/repository/db.go b/backend/repository/db.go index f469f34..8ca19ac 100644 --- a/backend/repository/db.go +++ b/backend/repository/db.go @@ -113,6 +113,48 @@ func (d *JsonDB) raiseLegacyAlertThresholds() { } } +// removeOrphanManagedLogSources 清掉"部署已不存在、但数据源还在"的孤儿记录。 +// +// 判据:Managed=true(平台托管)且没有任何同名部署。 +// 连带:删掉指向它的查询授权策略组,并禁用绑定它的告警规则。 +func (d *JsonDB) removeOrphanManagedLogSources() { + alive := map[string]bool{} + for _, dep := range d.Deployments { + alive[dep.Name] = true + } + orphans := map[uint]bool{} + kept := make([]models.LogSource, 0, len(d.LogSources)) + for _, src := range d.LogSources { + if src.Managed && !alive[src.Name] { + orphans[src.ID] = true + log.Printf("[schema] 移除孤儿数据源 %q(id=%d):对应部署已删除", src.Name, src.ID) + continue + } + kept = append(kept, src) + } + if len(orphans) == 0 { + return + } + d.LogSources = kept + + groups := make([]models.LogPolicyGroup, 0, len(d.LogPolicyGroups)) + for _, group := range d.LogPolicyGroups { + if orphans[group.SourceID] { + log.Printf("[schema] 移除指向孤儿数据源的策略组 %q", group.Name) + continue + } + groups = append(groups, group) + } + d.LogPolicyGroups = groups + + for i := range d.AlertRules { + if orphans[d.AlertRules[i].SourceID] { + d.AlertRules[i].Enabled = false + log.Printf("[schema] 告警规则 %q 的数据源已删除,规则已禁用", d.AlertRules[i].Name) + } + } +} + // linkRulesToDefaultChannel 给未关联通道的规则挂上默认通道。 // // 没有通道时不凭空造(那是 seed 首次部署的职责);这里只在已有通道时补齐关联。 @@ -366,7 +408,7 @@ func (d *JsonDB) dedupeLogSourceIDs() { // // v0/v1(无字段)→ 1:新增 auditLogs / schemaVersion(老库走零值兜底即可,无需处理) // v1 → 2:预留示例(如把 ScrapeJobs 从 logPath 拆分时,需按旧结构生成默认 job) -const CurrentSchemaVersion = 6 +const CurrentSchemaVersion = 7 // migrateSchema 按版本号顺序迁移老库。返回是否发生了迁移(需要回写)。 func (d *JsonDB) migrateSchema() bool { @@ -426,6 +468,12 @@ func (d *JsonDB) migrateSchema() bool { // notify-config;那个早就不用了,结果就是"规则在响但哪里都没发出去"。 d.linkRulesToDefaultChannel() d.SchemaVersion = 6 + case 6: + // v6 → v7:清理"孤儿数据源"。 + // 删除部署时曾漏掉对自动注册数据源的清理,导致查询页残留指向 + // 已删实例的数据源(线上残留过 host-verify-loki)。 + d.removeOrphanManagedLogSources() + d.SchemaVersion = 7 default: // 防御:出现未知版本(比当前代码还新)时不乱动,避免降级把数据写坏 log.Printf("[schema] 数据库版本 %d 高于当前代码支持版本 %d,跳过迁移", d.SchemaVersion, CurrentSchemaVersion) diff --git a/backend/services/deployment.go b/backend/services/deployment.go index 06307b0..23f9a24 100644 --- a/backend/services/deployment.go +++ b/backend/services/deployment.go @@ -325,15 +325,26 @@ func (s *DeploymentService) Delete(id uint) error { } db := repository.GetDB() db.Lock() - defer db.Unlock() - for i, d := range db.Deployments { - if d.ID == id { + removedName := "" + for i := range db.Deployments { + if db.Deployments[i].ID == id { + removedName = db.Deployments[i].Name db.Deployments = append(db.Deployments[:i], db.Deployments[i+1:]...) db.RequestSave() - return nil + break } } - return utils.ErrNotFound + // 必须先解锁再清理数据源:CleanupManagedLogSource 内部会自己加锁, + // 持有 db.Lock() 时再调用会死锁。 + db.Unlock() + if removedName == "" { + return utils.ErrNotFound + } + // 同步清理该部署自动注册的查询数据源。 + // 之前漏了这一步:删完部署后「日志查询」里还留着一个指向已删实例的数据源, + // 选中它必然连接失败(线上就残留过 host-verify-loki)。 + CleanupManagedLogSource(removedName) + return nil } // ==================== Goroutine 管理改进 ==================== diff --git a/backend/services/log_source.go b/backend/services/log_source.go index df5ec3b..182cb00 100644 --- a/backend/services/log_source.go +++ b/backend/services/log_source.go @@ -4,6 +4,7 @@ package services import ( "encoding/json" "fmt" + "log" "regexp" "sort" "strings" @@ -384,6 +385,54 @@ func reachableEndpoint(d *models.LokiDeployment) string { return endpoint } +// CleanupManagedLogSource 删除某部署名对应的**平台托管**数据源。 +// +// 为什么必须有:部署删除后它自动注册的数据源若不清掉,「日志查询」下拉里会 +// 留一个指向已删实例的源,选中必然连接失败(线上残留过 host-verify-loki)。 +// +// 连带处理: +// - 该源上的查询授权策略组:DeleteLogSource 内部会一并删除 +// - 绑定了该源的告警规则:置为禁用(保留配置便于审计,但不再触发, +// 否则引擎每次评估都会因为连不上而写失败记录) +// - 手填的数据源(Managed=false)不动:那是用户自己建的,可能指向别的实例 +func CleanupManagedLogSource(name string) { + db := repository.GetDB() + if db == nil || name == "" { + return + } + db.RLock() + var sourceID uint + for _, src := range db.LogSources { + if src.Managed && src.Name == name { + sourceID = src.ID + break + } + } + db.RUnlock() + if sourceID == 0 { + // 部署本来就没注册数据源(如部署失败),无需处理 + return + } + + if err := DeleteLogSource(sourceID); err != nil { + log.Printf("[deploy-delete] 清理数据源 %q(id=%d) 失败: %v", name, sourceID, err) + } + + db.Lock() + defer db.Unlock() + disabled := 0 + for i := range db.AlertRules { + if db.AlertRules[i].SourceID == sourceID && db.AlertRules[i].Enabled { + db.AlertRules[i].Enabled = false + disabled++ + } + } + if disabled > 0 { + db.RequestSave() + log.Printf("[deploy-delete] %d 条告警规则因数据源被删而禁用", disabled) + } +} + // EnsureLogSourceForDeployment 部署成功后自动注册查询数据源(host→url;k8s→cluster-svc)。 // 已存在同名数据源时:平台托管的自动刷新连接信息,用户手填的不覆盖。 func EnsureLogSourceForDeployment(d *models.LokiDeployment) {