From bd2183c8bbf1173580457662d38339be430c1d3e Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 9 Aug 2026 09:04:33 +0800 Subject: [PATCH] feat(log): add independent short retention for performance metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 性能指标(CPU/内存/磁盘/网络)不再跟随 log_retention_days_*,新增三库共用 的 metric_retention_days 配置(默认 3 天),系统垃圾清理按独立短留存清理 指标快照;访问日志保留时长不变。新增 PG/SQLite 双方言 goose 迁移 seed。 --- docs/changelog/index.md | 1 + docs/reference/configuration.md | 9 +- internal/apps/upload/task/cleanup.go | 6 +- .../202608090001_metric_retention_config.sql | 9 ++ .../202608090001_metric_retention_config.sql | 8 ++ internal/model/system_configs.go | 3 + internal/repository/logstore/cleanup.go | 38 +++++++- internal/repository/logstore/cleanup_test.go | 92 +++++++++++++++++++ 8 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 internal/infra/persistence/migrator/goose/postgres/202608090001_metric_retention_config.sql create mode 100644 internal/infra/persistence/migrator/goose/sqlite/202608090001_metric_retention_config.sql diff --git a/docs/changelog/index.md b/docs/changelog/index.md index fd49705a..c35bd8cc 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -28,6 +28,7 @@ sidebar: false ### 变更 +- 性能指标(CPU/内存/磁盘/网络)与访问日志的保留时长解耦:新增三库共用的 `metric_retention_days` 配置(默认 3 天),系统垃圾清理每日任务按独立短留存清理指标快照;访问日志仍按 `log_retention_days_*` 清理。 - ClickHouse 改为默认关闭:`clickhouse.enabled` 缺省或为 `false` 时不启用(此前会被强制置为 `true`),日志/指标由 PostgreSQL/SQLite 主库承担;显式 `true` 或设置 `CLICKHOUSE_HOST` / `CLICKHOUSE_ENABLED=true` 时启用。 ### 修复 diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index cea51217..0a3d1e9f 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -272,15 +272,16 @@ Server 的所有核心基础配置定义在 `config.yaml` 中,且均支持环 ### 8. 日志存储(Log Database) -日志存储解耦后的运行时配置:日志主库由「切换日志数据库」任务管理(内部/受保护 key,禁止管理员手动修改),保留天数按存储库分别在业务配置中设置。 +日志存储解耦后的运行时配置:日志主库由「切换日志数据库」任务管理(内部/受保护 key,禁止管理员手动修改),访问日志保留天数按存储库分别在业务配置中设置;性能指标(CPU/内存/磁盘/网络)价值衰减快,按三库共用的独立短留存清理。 | 配置键 (Key) | 数据类型 | 作用说明 | 默认值 | | --- | --- | --- | --- | | `log_database` | `string` | 当前日志主库(`postgres` / `sqlite` / `clickhouse`)。**内部受保护 key**:仅「切换日志数据库」迁移任务写入,管理员不可手动创建/修改 | 随主库(PostgreSQL 启用时为 `postgres`,否则 `sqlite`;ClickHouse 启用时优先 `clickhouse`) | | `log_db_migration` | `string` | 日志迁移冻结标记(`migrating` 或空)。**内部受保护 key**:仅迁移任务写入,置位期间日志写入返回 503「日志数据库迁移中,暂不可写」 | 空 | -| `log_retention_days_postgres` | `int` | PostgreSQL 日志库的过期清理保留天数(过期日志由系统垃圾清理每日任务删除) | `90` | -| `log_retention_days_sqlite` | `int` | SQLite 日志库的过期清理保留天数 | `90` | -| `log_retention_days_clickhouse` | `int` | ClickHouse 日志库的过期清理保留天数 | `90` | +| `log_retention_days_postgres` | `int` | PostgreSQL 日志库的访问日志过期清理保留天数(过期日志由系统垃圾清理每日任务删除) | `90` | +| `log_retention_days_sqlite` | `int` | SQLite 日志库的访问日志过期清理保留天数 | `90` | +| `log_retention_days_clickhouse` | `int` | ClickHouse 日志库的访问日志过期清理保留天数 | `90` | +| `metric_retention_days` | `int` | 性能指标(CPU/内存/磁盘/网络)保留天数,三库共用独立短留存(不随访问日志保留配置) | `3` | --- diff --git a/internal/apps/upload/task/cleanup.go b/internal/apps/upload/task/cleanup.go index 378cf106..84ff11de 100644 --- a/internal/apps/upload/task/cleanup.go +++ b/internal/apps/upload/task/cleanup.go @@ -130,16 +130,16 @@ func (h *SystemCleanupHandler) Execute(ctx context.Context, _ []byte) (*task.Tas ) } - task.AppendLog(ctx, "开始清理过期日志(按当前日志库保留天数)...") + task.AppendLog(ctx, "开始清理过期日志(访问日志按当前日志库保留天数,性能指标按独立短留存)...") summary, err := logstore.CleanupExpired(ctx) switch { case err != nil: logger.ErrorF(ctx, "清理过期日志失败: %v", err) task.AppendLog(ctx, "清理过期日志失败: %v", err) case summary.Deleted == 0: - task.AppendLog(ctx, "没有需要清理的过期日志 (保留 %d 天)", summary.RetentionDays) + task.AppendLog(ctx, "没有需要清理的过期日志 (访问日志保留 %d 天,性能指标保留 %d 天)", summary.RetentionDays, summary.MetricRetentionDays) default: - task.AppendLog(ctx, "日志清理完成:保留 %d 天,删除 %d 条", summary.RetentionDays, summary.Deleted) + task.AppendLog(ctx, "日志清理完成:访问日志保留 %d 天,性能指标保留 %d 天,删除 %d 条", summary.RetentionDays, summary.MetricRetentionDays, summary.Deleted) } msg := fmt.Sprintf("系统清理完成。成功清理未使用的上传文件 %d/%d 个;清理历史推送审计日志 %d 条;清理任务执行日志 %d 条。", diff --git a/internal/infra/persistence/migrator/goose/postgres/202608090001_metric_retention_config.sql b/internal/infra/persistence/migrator/goose/postgres/202608090001_metric_retention_config.sql new file mode 100644 index 00000000..0d3b5912 --- /dev/null +++ b/internal/infra/persistence/migrator/goose/postgres/202608090001_metric_retention_config.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- 性能指标(CPU/内存/磁盘/网络)保留天数:三库共用、独立短留存(默认 3 天), +-- 不随 log_retention_days_*(访问日志保留配置)变化。 +INSERT INTO w_system_configs (key, value, type, visibility, description, created_at, updated_at) +VALUES ('metric_retention_days', '3', 'business', 0, '性能指标(CPU/内存/磁盘/网络)保留天数(三库共用)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) +ON CONFLICT (key) DO NOTHING; + +-- +goose Down +DELETE FROM w_system_configs WHERE key = 'metric_retention_days'; diff --git a/internal/infra/persistence/migrator/goose/sqlite/202608090001_metric_retention_config.sql b/internal/infra/persistence/migrator/goose/sqlite/202608090001_metric_retention_config.sql new file mode 100644 index 00000000..7c1b6d32 --- /dev/null +++ b/internal/infra/persistence/migrator/goose/sqlite/202608090001_metric_retention_config.sql @@ -0,0 +1,8 @@ +-- +goose Up +-- 性能指标(CPU/内存/磁盘/网络)保留天数:三库共用、独立短留存(默认 3 天), +-- 不随 log_retention_days_*(访问日志保留配置)变化。 +INSERT OR IGNORE INTO w_system_configs (key, value, type, visibility, description, created_at, updated_at) +VALUES ('metric_retention_days', '3', 'business', 0, '性能指标(CPU/内存/磁盘/网络)保留天数(三库共用)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +-- +goose Down +DELETE FROM w_system_configs WHERE key = 'metric_retention_days'; diff --git a/internal/model/system_configs.go b/internal/model/system_configs.go index c3996f31..aa3ec19a 100644 --- a/internal/model/system_configs.go +++ b/internal/model/system_configs.go @@ -127,6 +127,9 @@ const ( ConfigKeyLogRetentionDaysPostgres = "log_retention_days_postgres" // PostgreSQL 日志保留天数 ConfigKeyLogRetentionDaysSQLite = "log_retention_days_sqlite" // SQLite 日志保留天数 ConfigKeyLogRetentionDaysClickHouse = "log_retention_days_clickhouse" // ClickHouse 日志保留天数 + // ConfigKeyMetricRetentionDays 性能指标(CPU/内存/磁盘/网络)保留天数,三库共用; + // 性能数据价值衰减快,默认短留存(3 天),不随访问日志保留配置。 + ConfigKeyMetricRetentionDays = "metric_retention_days" ) const ( diff --git a/internal/repository/logstore/cleanup.go b/internal/repository/logstore/cleanup.go index ea800178..91c617de 100644 --- a/internal/repository/logstore/cleanup.go +++ b/internal/repository/logstore/cleanup.go @@ -17,8 +17,11 @@ import ( // CleanupSummary 汇总本次清理结果。 type CleanupSummary struct { ActiveDatabase string `json:"active_database"` - RetentionDays int `json:"retention_days"` - Deleted int64 `json:"deleted"` + // RetentionDays 访问日志(节点访问/用户访问)保留天数,按日志库读取。 + RetentionDays int `json:"retention_days"` + // MetricRetentionDays 性能指标(CPU/内存/磁盘/网络)保留天数,三库共用短留存。 + MetricRetentionDays int `json:"metric_retention_days"` + Deleted int64 `json:"deleted"` // Tables 记录本次清理的物理表简写名(去掉 of_ 前缀,如 node_access_logs 对应 // of_node_access_logs;CH 侧物理表名相同,简写仅便于状态展示)。 Tables []string `json:"tables"` @@ -27,6 +30,10 @@ type CleanupSummary struct { // defaultLogRetentionDays 默认日志保留天数(配置缺失/非法时回退)。 const defaultLogRetentionDays = 90 +// defaultMetricRetentionDays 默认性能指标保留天数(配置缺失/非法时回退)。 +// 性能数据价值衰减快,默认短留存(3 天)。 +const defaultMetricRetentionDays = 3 + // partitionLeadMonths 清理时确保「当前月 + 未来 2 个月」分区持续存在。 const partitionLeadMonths = 2 @@ -54,7 +61,26 @@ func retentionDaysForDatabase(ctx context.Context, dbName string) int { return days } -// CleanupExpired 按当前激活库保留天数清理过期日志(每日由 system_cleanup 调用)。 +// metricRetentionDays 读取性能指标保留天数(三库共用,默认 3 天)。 +func metricRetentionDays(ctx context.Context) int { + v, err := getConfig(ctx, model.ConfigKeyMetricRetentionDays) + if err != nil { + if !errors.Is(err, errConfigReaderNotWired) { + logger.ErrorF(ctx, "读取性能指标保留天数配置失败(key=%s),回退默认 %d 天: %v", model.ConfigKeyMetricRetentionDays, defaultMetricRetentionDays, err) + } + return defaultMetricRetentionDays + } + days, perr := strconv.Atoi(v) + if perr != nil || days <= 0 { + logger.ErrorF(ctx, "性能指标保留天数配置非法(key=%s, value=%q),回退默认 %d 天", model.ConfigKeyMetricRetentionDays, v, defaultMetricRetentionDays) + return defaultMetricRetentionDays + } + return days +} + +// CleanupExpired 按当前激活库保留天数清理过期日志(每日由 system_cleanup 调用): +// 访问日志(节点访问/用户访问)按 log_retention_days_* 清理; +// 性能指标(CPU/内存/磁盘/网络)按三库共用的短留存 metric_retention_days 清理。 func CleanupExpired(ctx context.Context) (*CleanupSummary, error) { dbName, err := resolveDatabase(ctx) if err != nil { @@ -65,8 +91,10 @@ func CleanupExpired(ctx context.Context) (*CleanupSummary, error) { return nil, err } days := retentionDaysForDatabase(ctx, dbName) + metricDays := metricRetentionDays(ctx) cutoff := time.Now().AddDate(0, 0, -days) - summary := &CleanupSummary{ActiveDatabase: dbName, RetentionDays: days, Tables: []string{}} + metricCutoff := time.Now().AddDate(0, 0, -metricDays) + summary := &CleanupSummary{ActiveDatabase: dbName, RetentionDays: days, MetricRetentionDays: metricDays, Tables: []string{}} // PG 分区表仅在迁移时预建「当前+2 月」分区,此处确保分区持续存在, // 否则跨月后新写入会报 "no partition of relation found"(SQLite/CH 为 no-op)。 @@ -81,7 +109,7 @@ func CleanupExpired(ctx context.Context) (*CleanupSummary, error) { return nil, err } if err := cleanupTable("metric_snapshots", func() (int64, error) { - return s.Observability.DeleteMetricSnapshotsBefore(ctx, cutoff) + return s.Observability.DeleteMetricSnapshotsBefore(ctx, metricCutoff) }, summary); err != nil { return nil, err } diff --git a/internal/repository/logstore/cleanup_test.go b/internal/repository/logstore/cleanup_test.go index 0a2d1796..e1e20e3f 100644 --- a/internal/repository/logstore/cleanup_test.go +++ b/internal/repository/logstore/cleanup_test.go @@ -57,6 +57,8 @@ func TestCleanupExpiredSQLite(t *testing.T) { return "sqlite", nil case model.ConfigKeyLogRetentionDaysSQLite: return "30", nil + case model.ConfigKeyMetricRetentionDays: + return "3", nil } return "", nil }) @@ -108,6 +110,9 @@ func TestCleanupExpiredSQLite(t *testing.T) { if summary.RetentionDays != 30 { t.Fatalf("RetentionDays = %d, want 30", summary.RetentionDays) } + if summary.MetricRetentionDays != 3 { + t.Fatalf("MetricRetentionDays = %d, want 3", summary.MetricRetentionDays) + } if summary.Deleted != 5 { t.Fatalf("Deleted = %d, want 5", summary.Deleted) } @@ -140,6 +145,93 @@ func TestCleanupExpiredSQLite(t *testing.T) { } } +// TestCleanupExpiredMetricShortRetention 回归:性能指标(CPU/内存/磁盘/网络)按三库共用 +// 的短留存(默认 3 天)清理,与访问日志保留天数(log_retention_days_*)解耦。 +// 10 天前的指标快照被删(> 3 天),同日期的访问日志保留(< 30 天)。 +func TestCleanupExpiredMetricShortRetention(t *testing.T) { + ResetForTest() + SetConfigReader(func(_ context.Context, key string) (string, error) { + switch key { + case logDatabaseKey: + return "sqlite", nil + case model.ConfigKeyLogRetentionDaysSQLite: + return "30", nil + case model.ConfigKeyMetricRetentionDays: + return "3", nil + } + return "", nil + }) + defer ResetForTest() + + gdb := newCleanupTestDB(t) + ctx := context.Background() + mid := time.Now().AddDate(0, 0, -10).UTC() // 10 天前:超指标留存、未超日志留存 + + if err := gdb.Create(&analyticsmodel.NodeAccessLog{ID: 1, NodeID: "n1", LoggedAt: mid, RemoteAddr: "1.1.1.1"}).Error; err != nil { + t.Fatalf("seed node access log: %v", err) + } + if err := gdb.Create(&analyticsmodel.NodeMetricSnapshot{ID: 1, NodeID: "n1", CapturedAt: mid}).Error; err != nil { + t.Fatalf("seed metric snapshot: %v", err) + } + + summary, err := CleanupExpired(ctx) + if err != nil { + t.Fatalf("CleanupExpired: %v", err) + } + if summary.RetentionDays != 30 || summary.MetricRetentionDays != 3 { + t.Fatalf("retention = (%d, %d), want (30, 3)", summary.RetentionDays, summary.MetricRetentionDays) + } + + var accessCount, metricCount int64 + if err := gdb.Model(&analyticsmodel.NodeAccessLog{}).Count(&accessCount).Error; err != nil { + t.Fatalf("count access logs: %v", err) + } + if err := gdb.Model(&analyticsmodel.NodeMetricSnapshot{}).Count(&metricCount).Error; err != nil { + t.Fatalf("count metric snapshots: %v", err) + } + if accessCount != 1 { + t.Fatalf("node_access_logs count = %d, want 1 (10 天在 30 天日志留存内)", accessCount) + } + if metricCount != 0 { + t.Fatalf("metric_snapshots count = %d, want 0 (10 天超 3 天指标留存)", metricCount) + } +} + +// TestMetricRetentionDays 覆盖性能指标保留天数读取:合法值、非法值回退默认 3。 +func TestMetricRetentionDays(t *testing.T) { + ResetForTest() + SetConfigReader(func(_ context.Context, key string) (string, error) { + switch key { + case model.ConfigKeyMetricRetentionDays: + return "5", nil + } + return "", nil + }) + if got := metricRetentionDays(context.Background()); got != 5 { + t.Fatalf("metricRetentionDays = %d, want 5", got) + } + + // 非法值(非数字/<=0)回退默认 3。 + SetConfigReader(func(_ context.Context, key string) (string, error) { + switch key { + case model.ConfigKeyMetricRetentionDays: + return "abc", nil + } + return "", nil + }) + if got := metricRetentionDays(context.Background()); got != defaultMetricRetentionDays { + t.Fatalf("metricRetentionDays invalid value = %d, want %d", got, defaultMetricRetentionDays) + } + + // reader 报错回退默认 3。 + SetConfigReader(func(_ context.Context, _ string) (string, error) { + return "", fmt.Errorf("boom") + }) + if got := metricRetentionDays(context.Background()); got != defaultMetricRetentionDays { + t.Fatalf("metricRetentionDays reader error = %d, want %d", got, defaultMetricRetentionDays) + } +} + // TestRetentionDaysForDatabase 覆盖保留天数读取:按激活库选 key、非法值回退默认 90。 func TestRetentionDaysForDatabase(t *testing.T) { ResetForTest()