mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-01 06:36:38 +08:00
136 lines
4.8 KiB
Go
136 lines
4.8 KiB
Go
// Copyright 2026 Arctel.net
|
||
// SPDX-License-Identifier: Apache-2.0
|
||
|
||
package logstore
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"strconv"
|
||
"time"
|
||
|
||
"github.com/Rain-kl/Wavelet/internal/model"
|
||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||
)
|
||
|
||
// CleanupSummary 汇总本次清理结果。
|
||
type CleanupSummary struct {
|
||
ActiveDatabase string `json:"active_database"`
|
||
RetentionDays int `json:"retention_days"`
|
||
Deleted int64 `json:"deleted"`
|
||
// Tables 记录本次清理的物理表简写名(去掉 of_ 前缀,如 node_access_logs 对应
|
||
// of_node_access_logs;CH 侧物理表名相同,简写仅便于状态展示)。
|
||
Tables []string `json:"tables"`
|
||
}
|
||
|
||
// defaultLogRetentionDays 默认日志保留天数(配置缺失/非法时回退)。
|
||
const defaultLogRetentionDays = 90
|
||
|
||
// partitionLeadMonths 清理时确保「当前月 + 未来 2 个月」分区持续存在。
|
||
const partitionLeadMonths = 2
|
||
|
||
// retentionDaysForDatabase 按给定日志库读取保留天数(默认 90)。
|
||
func retentionDaysForDatabase(ctx context.Context, dbName string) int {
|
||
key := model.ConfigKeyLogRetentionDaysPostgres
|
||
switch dbName {
|
||
case dbNameSQLite:
|
||
key = model.ConfigKeyLogRetentionDaysSQLite
|
||
case dbNameClickHouse:
|
||
key = model.ConfigKeyLogRetentionDaysClickHouse
|
||
}
|
||
v, err := getConfig(ctx, key)
|
||
if err != nil {
|
||
if !errors.Is(err, errConfigReaderNotWired) {
|
||
logger.ErrorF(ctx, "读取日志保留天数配置失败(key=%s),回退默认 %d 天: %v", key, defaultLogRetentionDays, err)
|
||
}
|
||
return defaultLogRetentionDays
|
||
}
|
||
days, perr := strconv.Atoi(v)
|
||
if perr != nil || days <= 0 {
|
||
logger.ErrorF(ctx, "日志保留天数配置非法(key=%s, value=%q),回退默认 %d 天", key, v, defaultLogRetentionDays)
|
||
return defaultLogRetentionDays
|
||
}
|
||
return days
|
||
}
|
||
|
||
// CleanupExpired 按当前激活库保留天数清理过期日志(每日由 system_cleanup 调用)。
|
||
func CleanupExpired(ctx context.Context) (*CleanupSummary, error) {
|
||
dbName, err := resolveDatabase(ctx)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("resolve active database: %w", err)
|
||
}
|
||
s, err := Active(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
days := retentionDaysForDatabase(ctx, dbName)
|
||
cutoff := time.Now().AddDate(0, 0, -days)
|
||
summary := &CleanupSummary{ActiveDatabase: dbName, RetentionDays: days, Tables: []string{}}
|
||
|
||
// PG 分区表仅在迁移时预建「当前+2 月」分区,此处确保分区持续存在,
|
||
// 否则跨月后新写入会报 "no partition of relation found"(SQLite/CH 为 no-op)。
|
||
now := time.Now().UTC()
|
||
if err := s.AccessLogs.EnsurePartitions(ctx, now, now.AddDate(0, partitionLeadMonths, 0)); err != nil {
|
||
return nil, fmt.Errorf("ensure partitions: %w", err)
|
||
}
|
||
|
||
if err := cleanupTable("node_access_logs", func() (int64, error) {
|
||
return s.AccessLogs.DeleteBefore(ctx, cutoff)
|
||
}, summary); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := cleanupTable("metric_snapshots", func() (int64, error) {
|
||
return s.Observability.DeleteMetricSnapshotsBefore(ctx, cutoff)
|
||
}, summary); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := cleanupTable("edge_health", func() (int64, error) {
|
||
return s.Observability.DeleteEdgeHealthBefore(ctx, cutoff)
|
||
}, summary); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := cleanupTable("obs_frps", func() (int64, error) {
|
||
return s.Observability.DeleteNodeObservationFrpsBefore(ctx, cutoff)
|
||
}, summary); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := cleanupTable("obs_frpc", func() (int64, error) {
|
||
return s.Observability.DeleteNodeObservationFrpcBefore(ctx, cutoff)
|
||
}, summary); err != nil {
|
||
return nil, err
|
||
}
|
||
return summary, nil
|
||
}
|
||
|
||
func cleanupTable(name string, fn func() (int64, error), summary *CleanupSummary) error {
|
||
n, err := fn()
|
||
if err != nil {
|
||
return fmt.Errorf("cleanup %s: %w", name, err)
|
||
}
|
||
summary.Deleted += n
|
||
summary.Tables = append(summary.Tables, name)
|
||
return nil
|
||
}
|
||
|
||
// partitionStatementsRange 生成覆盖 [from, to] 全部月份的两表分区 DDL,
|
||
// 幂等 CREATE TABLE IF NOT EXISTS ... PARTITION OF ... FOR VALUES FROM ... TO ...。
|
||
// 入参为任意时间点:按各自所在月份生成,含 from 月与 to 月(to 常用 max+1 月兜底)。
|
||
func partitionStatementsRange(from, to time.Time) []string {
|
||
var out []string
|
||
start := time.Date(from.Year(), from.Month(), 1, 0, 0, 0, 0, time.UTC)
|
||
end := time.Date(to.Year(), to.Month(), 1, 0, 0, 0, 0, time.UTC).AddDate(0, 1, 0)
|
||
for ; start.Before(end); start = start.AddDate(0, 1, 0) {
|
||
monthEnd := start.AddDate(0, 1, 0)
|
||
suffix := start.Format("200601")
|
||
fromDay := start.Format("2006-01-02")
|
||
toDay := monthEnd.Format("2006-01-02")
|
||
for _, table := range []string{"of_node_access_logs", "w_user_access_logs"} {
|
||
out = append(out, fmt.Sprintf(
|
||
"CREATE TABLE IF NOT EXISTS %s_%s PARTITION OF %s FOR VALUES FROM ('%s') TO ('%s')",
|
||
table, suffix, table, fromDay, toDay))
|
||
}
|
||
}
|
||
return out
|
||
}
|