diff --git a/backend/controllers/logs.go b/backend/controllers/logs.go index 1472358..2147e93 100644 --- a/backend/controllers/logs.go +++ b/backend/controllers/logs.go @@ -166,9 +166,11 @@ func (ctrl *LogsController) DeleteSource(c *gin.Context) { utils.BadRequest(c, "ID 无效") return } + // 托管数据源也允许删除:用户可能只是不想让它出现在查询页下拉里。 + // 删除后把对应部署的 AutoSourceDisabled 置位,否则列表接口/启动同步 + // 会立刻把它补回来(用户看到的现象就是"删了还在")。 if src := services.GetLogSource(uint(id)); src != nil && src.Managed { - utils.BadRequest(c, "平台部署的数据源随部署管理,请先删除对应部署") - return + services.DisableAutoSourceFor(src.Name) } if err := services.DeleteLogSource(uint(id)); err != nil { utils.Error(c, 400, err.Error()) diff --git a/backend/models/deployment.go b/backend/models/deployment.go index 42aa36c..96a6c42 100644 --- a/backend/models/deployment.go +++ b/backend/models/deployment.go @@ -27,18 +27,23 @@ type LokiDeployment struct { Replicas int `json:"replicas"` ResourcesCPU string `json:"resourcesCPU"` ResourcesMem string `json:"resourcesMem"` - StorageSize string `json:"storageSize"` - GrafanaEnabled bool `json:"grafanaEnabled"` - GrafanaVersion string `json:"grafanaVersion"` - GrafanaPort int `json:"grafanaPort"` - GrafanaEndpoint string `json:"grafanaEndpoint"` - Status string `json:"status"` - StatusMessage string `json:"statusMessage"` - Endpoint string `json:"endpoint"` - AdminUser string `json:"adminUser"` - AdminPassword string `json:"adminPassword"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + // AutoSourceDisabled 用户手动删除了"自动注册的数据源"后置为 true, + // 平台不再自动重建,直到用户重新部署/编辑该部署。 + // 没有这个开关的话:删了数据源 → 下次打开查询页又被 EnsureLogSources 补回来, + // 用户看到的现象就是"删不掉"。 + AutoSourceDisabled bool `json:"autoSourceDisabled"` + StorageSize string `json:"storageSize"` + GrafanaEnabled bool `json:"grafanaEnabled"` + GrafanaVersion string `json:"grafanaVersion"` + GrafanaPort int `json:"grafanaPort"` + GrafanaEndpoint string `json:"grafanaEndpoint"` + Status string `json:"status"` + StatusMessage string `json:"statusMessage"` + Endpoint string `json:"endpoint"` + AdminUser string `json:"adminUser"` + AdminPassword string `json:"adminPassword"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } func (d *LokiDeployment) BeforeCreate() { diff --git a/backend/repository/db.go b/backend/repository/db.go index 4d61b55..223e04d 100644 --- a/backend/repository/db.go +++ b/backend/repository/db.go @@ -114,6 +114,51 @@ func (d *JsonDB) raiseLegacyAlertThresholds() { } } +// compactAlertWindows 把告警窗口从 15 分钟压到 5 分钟,阈值按 1/3 同步下调。 +// +// 为什么:检查间隔已经调到 5 分钟,而窗口还是 15 分钟 —— +// 相当于同一个 15 分钟窗口每 5 分钟被完整重扫一遍(3 倍重复扫描)。 +// 在 370GB/天、全命名空间 selector 下,这是 Loki CPU 的最大稳态来源。 +// +// 只改"仍是旧默认值"的规则(窗口 15 且阈值等于旧默认),用户自己调过的不动。 +func (d *JsonDB) compactAlertWindows() { + type change struct { + window int + threshold int + newName string + } + // 旧默认阈值 → 新值(窗口统一 5 分钟,阈值约 1/3) + byOldThreshold := map[string]change{ + "致命日志 FATAL": {window: 5, threshold: 4}, + "Go panic": {window: 5, threshold: 4}, + "JVM OutOfMemoryError": {window: 5, threshold: 2}, + "StackOverflowError": {window: 5, threshold: 2}, + "应用错误激增(error > 10万)": {window: 5, threshold: 35000, newName: "应用错误激增(error > 3.5万)"}, + "ingress-nginx 5xx 错误": {window: 5, threshold: 700}, + "ingress-nginx 4xx 激增": {window: 5, threshold: 7000}, + "数据库连接失败": {window: 5, threshold: 350}, + } + for i := range d.AlertRules { + rule := &d.AlertRules[i] + if rule.WindowMin != 15 { + continue // 用户改过窗口,不动 + } + spec, ok := byOldThreshold[rule.Name] + if !ok { + continue + } + oldThreshold := rule.Threshold + oldWindow := rule.WindowMin + rule.WindowMin = spec.window + rule.Threshold = spec.threshold + if spec.newName != "" { + rule.Name = spec.newName + } + log.Printf("[schema] 告警规则 %q 窗口 %dm → %dm,阈值 %d → %d(消除重复扫描)", + rule.Name, oldWindow, rule.WindowMin, oldThreshold, rule.Threshold) + } +} + // expandDatabaseErrorPattern 把内置「数据库连接失败」规则升级为覆盖 // MySQL / PostgreSQL / Redis 的主流报错文案。 // @@ -441,7 +486,7 @@ func (d *JsonDB) dedupeLogSourceIDs() { // // v0/v1(无字段)→ 1:新增 auditLogs / schemaVersion(老库走零值兜底即可,无需处理) // v1 → 2:预留示例(如把 ScrapeJobs 从 logPath 拆分时,需按旧结构生成默认 job) -const CurrentSchemaVersion = 10 +const CurrentSchemaVersion = 11 // migrateSchema 按版本号顺序迁移老库。返回是否发生了迁移(需要回写)。 func (d *JsonDB) migrateSchema() bool { @@ -524,6 +569,12 @@ func (d *JsonDB) migrateSchema() bool { // Redis 的 NOAUTH / WRONGPASS / connection pool timeout 等常见报错) d.expandDatabaseErrorPattern() d.SchemaVersion = 10 + case 10: + // v10 → v11:告警窗口 15 分钟 → 5 分钟(与检查间隔对齐), + // 阈值同比例缩小,避免"窗口变短导致更容易触发"。 + // 目标:消除同一个 15 分钟窗口被重复扫描 3 次的浪费(Loki CPU 降到 1/3 扫描量)。 + d.compactAlertWindows() + d.SchemaVersion = 11 default: // 防御:出现未知版本(比当前代码还新)时不乱动,避免降级把数据写坏 log.Printf("[schema] 数据库版本 %d 高于当前代码支持版本 %d,跳过迁移", d.SchemaVersion, CurrentSchemaVersion) diff --git a/backend/seed/alert_rules.go b/backend/seed/alert_rules.go index 7fdb442..dd08a65 100644 --- a/backend/seed/alert_rules.go +++ b/backend/seed/alert_rules.go @@ -72,10 +72,10 @@ var builtinRules = []builtinRule{ // —— 致命类:这类日志本该极少出现,但也不能一条就报 —— // 全命名空间类查询一律排除 app=loki:Loki 会把执行过的查询语句记成自身 info 日志, // 查询文本包含规则关键字时告警会"命中自己",造成持续误报。 - {name: "致命日志 FATAL", query: `{namespace=~".+", app!="loki"} |= "FATAL"`, windowMin: 15, threshold: 10, cooldown: 120}, - {name: "Go panic", query: `{namespace=~".+", app!="loki"} |= "panic:"`, windowMin: 15, threshold: 10, cooldown: 120}, - {name: "JVM OutOfMemoryError", query: `{namespace=~".+", app!="loki"} |~ "(^|[^a-zA-Z])OutOfMemoryError"`, windowMin: 15, threshold: 5, cooldown: 120}, - {name: "StackOverflowError", query: `{namespace=~".+", app!="loki"} |= "StackOverflowError"`, windowMin: 15, threshold: 5, cooldown: 120}, + {name: "致命日志 FATAL", query: `{namespace=~".+", app!="loki"} |= "FATAL"`, windowMin: 5, threshold: 4, cooldown: 120}, + {name: "Go panic", query: `{namespace=~".+", app!="loki"} |= "panic:"`, windowMin: 5, threshold: 4, cooldown: 120}, + {name: "JVM OutOfMemoryError", query: `{namespace=~".+", app!="loki"} |~ "(^|[^a-zA-Z])OutOfMemoryError"`, windowMin: 5, threshold: 2, cooldown: 120}, + {name: "StackOverflowError", query: `{namespace=~".+", app!="loki"} |= "StackOverflowError"`, windowMin: 5, threshold: 2, cooldown: 120}, // 注意:这里**只放日志类告警**。 // 「节点 NotReady」「容器 OOMKilled」属于基础设施指标监控(kube-state-metrics / // Prometheus)的职责,用日志去匹配这些字符串既不可靠(日志没开或轮转就漏), @@ -84,12 +84,12 @@ var builtinRules = []builtinRule{ // 第二轮上调(500~5000 这一档实测仍然太敏感): // 这几个量级在业务高峰期本来就是常态,阈值贴着基线就等于"每天准点报警"。 // 原则:宁可只报"明显异常",也不要让人对告警脱敏。 - {name: "应用错误激增(error > 10万)", query: `{namespace=~".+", app!="loki"} |~ "(?i)\berror\b"`, windowMin: 15, threshold: 100000, cooldown: 240}, - {name: "ingress-nginx 5xx 错误", query: `{app=~"ingress-nginx|nginx-ingress-controller"} |~ " (50[0-4]|5[2-9][0-9]) "`, windowMin: 15, threshold: 2000, cooldown: 240}, - {name: "ingress-nginx 4xx 激增", query: `{app=~"ingress-nginx|nginx-ingress-controller"} |~ " 4[0-9][0-9] "`, windowMin: 15, threshold: 20000, cooldown: 240}, + {name: "应用错误激增(error > 3.5万)", query: `{namespace=~".+", app!="loki"} |~ "(?i)\berror\b"`, windowMin: 5, threshold: 35000, cooldown: 240}, + {name: "ingress-nginx 5xx 错误", query: `{app=~"ingress-nginx|nginx-ingress-controller"} |~ " (50[0-4]|5[2-9][0-9]) "`, windowMin: 5, threshold: 700, cooldown: 240}, + {name: "ingress-nginx 4xx 激增", query: `{app=~"ingress-nginx|nginx-ingress-controller"} |~ " 4[0-9][0-9] "`, windowMin: 5, threshold: 7000, cooldown: 240}, // 数据库连接类错误:覆盖 MySQL / PostgreSQL / Redis 的主流报错文案 // (详见函数上方注释,不要加裸 "EOF" / "timeout",那会匹配到几乎所有网络日志) - {name: "数据库连接失败", query: `{namespace=~".+", app!="loki"} |~ "(?i)(connection refused|connection reset by peer|broken pipe|no route to host|cannot assign requested address|connection timed out|i/o timeout|too many connections|too many clients already|max number of clients reached|connection pool|connection is not available|could not create connection|communications link failure|server closed the connection unexpectedly|unexpected EOF|driver: bad connection|FATAL:\\s*(password authentication failed|sorry, too many clients|terminating connection)|NOAUTH|WRONGPASS|READONLY you can.t write|CLUSTERDOWN)"`, windowMin: 15, threshold: 1000, cooldown: 240}, + {name: "数据库连接失败", query: `{namespace=~".+", app!="loki"} |~ "(?i)(connection refused|connection reset by peer|broken pipe|no route to host|cannot assign requested address|connection timed out|i/o timeout|too many connections|too many clients already|max number of clients reached|connection pool|connection is not available|could not create connection|communications link failure|server closed the connection unexpectedly|unexpected EOF|driver: bad connection|FATAL:\\s*(password authentication failed|sorry, too many clients|terminating connection)|NOAUTH|WRONGPASS|READONLY you can.t write|CLUSTERDOWN)"`, windowMin: 5, threshold: 350, cooldown: 240}, } // seedBuiltinAlertRules 为每个平台管理的 LogSource(managed=true)补齐内置规则: diff --git a/backend/services/deployment.go b/backend/services/deployment.go index a4f4025..2aac692 100644 --- a/backend/services/deployment.go +++ b/backend/services/deployment.go @@ -229,6 +229,13 @@ func (s *DeploymentService) List(page, pageSize int) ([]models.LokiDeployment, i } func (s *DeploymentService) Update(id uint, updates map[string]interface{}) error { + // 用户编辑部署 = 明确表达"这个部署我还在用",重新允许自动注册数据源。 + // (对应地,用户在数据源列表里删掉自动源后不会再被补回来,直到这里重置) + defer func() { + if d, err := s.Get(id); err == nil && d != nil { + EnableAutoSourceFor(d.Name) + } + }() if value, exists := updates["retentionDays"]; exists { days, ok := value.(float64) if !ok || days < 1 || days != float64(int(days)) { diff --git a/backend/services/log_source.go b/backend/services/log_source.go index af597e1..653c56e 100644 --- a/backend/services/log_source.go +++ b/backend/services/log_source.go @@ -518,6 +518,10 @@ func EnsureLogSourceForDeployment(d *models.LokiDeployment) { if d == nil || d.Endpoint == "" { return } + // 用户主动删过这个自动数据源 → 不再补回来(重新部署/编辑部署会重置该开关) + if d.AutoSourceDisabled { + return + } svc := "loki" port := 3100 target := &models.LogSource{ @@ -570,6 +574,42 @@ func EnsureLogSourceForDeployment(d *models.LokiDeployment) { _, _ = CreateLogSource(target) } +// DisableAutoSourceFor 标记某个部署不再自动注册数据源。 +// +// 触发场景:用户在「日志查询 → 数据源」里删掉了平台自动创建的数据源。 +// 不置位的话,EnsureLogSourcesForRunningDeployments 会立刻把它补回来。 +func DisableAutoSourceFor(name string) { + db := repository.GetDB() + if db == nil { + return + } + db.Lock() + defer db.Unlock() + for i := range db.Deployments { + if db.Deployments[i].Name == name && !db.Deployments[i].AutoSourceDisabled { + db.Deployments[i].AutoSourceDisabled = true + log.Printf("[log-source] %q 的自动数据源已被用户删除,不再自动重建(重新部署可恢复)", name) + db.RequestSave() + } + } +} + +// EnableAutoSourceFor 重新允许自动注册(部署被编辑/重新部署时调用)。 +func EnableAutoSourceFor(name string) { + db := repository.GetDB() + if db == nil { + return + } + db.Lock() + defer db.Unlock() + for i := range db.Deployments { + if db.Deployments[i].Name == name && db.Deployments[i].AutoSourceDisabled { + db.Deployments[i].AutoSourceDisabled = false + db.RequestSave() + } + } +} + // EnsureLogSourcesForRunningDeployments 补齐运行中部署的数据源,避免查询页默认没有数据源。 func EnsureLogSourcesForRunningDeployments() { db := repository.GetDB() diff --git a/frontend/src/i18n/locales/en/logs.ts b/frontend/src/i18n/locales/en/logs.ts index b6b9799..7ca31a5 100644 --- a/frontend/src/i18n/locales/en/logs.ts +++ b/frontend/src/i18n/locales/en/logs.ts @@ -44,6 +44,8 @@ export default { testing: 'Testing…', deleteTitle: 'Delete data source', deleteDesc: 'Deleting "{{name}}" breaks queries and access policies based on it. Delete it?', + deleteDescManaged: '"{{name}}" was created automatically by a deployment. The platform will NOT recreate it (access policies are removed too) until you redeploy or edit that Loki deployment.', + managedEditHint: 'This source is created by Component Deploy: redeploying overwrites the cluster/service/port connection details; auth and header changes you make here are kept.', deleteAria: 'Delete {{name}}', defaultBadge: 'Default', emptyTitle: 'No data sources yet', diff --git a/frontend/src/i18n/locales/zh/logs.ts b/frontend/src/i18n/locales/zh/logs.ts index 6d949f9..d2c9c91 100644 --- a/frontend/src/i18n/locales/zh/logs.ts +++ b/frontend/src/i18n/locales/zh/logs.ts @@ -43,6 +43,8 @@ export default { testing: '测试中…', deleteTitle: '删除数据源', deleteDesc: '删除「{{name}}」后,基于它的查询与授权策略将失效,确定删除吗?', + deleteDescManaged: '「{{name}}」由部署自动创建。删除后平台不会再自动重建它(授权策略一并失效);重新部署或编辑该 Loki 部署才会恢复。', + managedEditHint: '该数据源由「组件部署」自动创建:重新部署会覆盖集群/Service/端口等连接信息,你在此修改的认证方式与请求头会保留。', deleteAria: '删除 {{name}}', defaultBadge: '默认', emptyTitle: '还没有数据源', diff --git a/frontend/src/pages/logs/components/SourcesManager.tsx b/frontend/src/pages/logs/components/SourcesManager.tsx index 669ef28..ede8cf6 100644 --- a/frontend/src/pages/logs/components/SourcesManager.tsx +++ b/frontend/src/pages/logs/components/SourcesManager.tsx @@ -127,23 +127,22 @@ export const SourcesManager = memo(function SourcesManager({ sources, onSaved }: {testing ? : } {testing ? t('source.testing') : t('source.test')} - {!managed && ( - - )} - {!managed && ( - - )} + {/* 托管源同样允许编辑/删除: + 托管只代表"由部署自动创建",不代表用户不能改连接参数或不想让它出现在下拉里。 + 删除托管源时后端会把该部署标记为 AutoSourceDisabled,不会立刻重建。 */} + + ) @@ -159,6 +158,11 @@ export const SourcesManager = memo(function SourcesManager({ sources, onSaved }: {form?.id ? `${t('source.editTitle')} · ${form.name}` : t('source.createTitle')} + {form?.id && form.managed && ( +

+ {t('source.managedEditHint')} +

+ )} {form && (