Files
junhong_cmp_fiber/cmd/audit-retention-simulate/main.go
break c64f3d8b80
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m31s
全局审计完成
2026-08-07 11:02:52 +08:00

321 lines
15 KiB
Go

// Command audit-retention-simulate 在测试环境演练完整自然月归档与清理边界。
package main
import (
"context"
"crypto/sha256"
"fmt"
"os"
"strings"
"time"
"github.com/bytedance/sonic"
"github.com/hibiken/asynq"
"go.uber.org/zap"
"gorm.io/datatypes"
"gorm.io/gorm"
auditarchive "github.com/break/junhong_cmp_fiber/internal/application/auditarchive"
auditinfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/model"
taskapp "github.com/break/junhong_cmp_fiber/internal/task"
"github.com/break/junhong_cmp_fiber/pkg/config"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/database"
"github.com/break/junhong_cmp_fiber/pkg/storage"
)
const (
simulationMonth = "2001-02"
simulationInstance = "retention-simulation-2001-02"
simulationPrefix = "retention-simulation-2001-02"
)
type simulationSummary struct {
Month string `json:"month"`
Days int `json:"days"`
ArchiveRuns int64 `json:"archive_runs"`
SuccessfulRuns int64 `json:"successful_runs"`
FinalIntegrationRuns int64 `json:"final_integration_runs"`
MaxRevision int `json:"max_revision"`
MaxAttemptCount int `json:"max_attempt_count"`
CompressionRatio float64 `json:"compression_ratio"`
DryRunEventCount int64 `json:"dry_run_event_count"`
DryRunResourceCount int64 `json:"dry_run_resource_count"`
DryRunIntegrationCount int64 `json:"dry_run_integration_count"`
EstimatedCleanupBatches int64 `json:"estimated_cleanup_batches"`
CleanupMarkersBefore int64 `json:"cleanup_markers_before"`
TargetRowsAfterCleanup int64 `json:"target_rows_after_cleanup"`
BoundaryRowsAfterCleanup int64 `json:"boundary_rows_after_cleanup"`
CleanupMarkersAfter int64 `json:"cleanup_markers_after"`
RetentionAuditRecorded bool `json:"retention_audit_recorded"`
}
func main() {
if err := run(context.Background()); err != nil {
fmt.Fprintln(os.Stderr, "归档留存仿真失败:", err)
os.Exit(1)
}
}
func run(ctx context.Context) error {
cfg, err := config.Load()
if err != nil {
return err
}
if !strings.Contains(strings.ToLower(cfg.Database.DBName), "test") || os.Getenv("JUNHONG_AUDIT_RETENTION_SIMULATION_CONFIRM") != cfg.Database.DBName {
return fmt.Errorf("仅允许显式确认的测试数据库,当前数据库为 %q", cfg.Database.DBName)
}
logger := zap.NewNop()
db, err := database.InitPostgreSQL(&cfg.Database, logger)
if err != nil {
return err
}
sqlDB, err := db.DB()
if err != nil {
return err
}
defer sqlDB.Close()
provider, err := storage.NewS3Provider(&cfg.Storage)
if err != nil {
return err
}
auditWriter := auditinfra.NewWriter(auditinfra.NewRegistry(), nil)
service, err := auditarchive.NewService(db, provider, simulationInstance, auditWriter)
if err != nil {
return err
}
location, err := time.LoadLocation(constants.AuditArchiveTimezone)
if err != nil {
return err
}
monthStart, _ := time.ParseInLocation("2006-01", simulationMonth, location)
monthEnd := monthStart.AddDate(0, 1, 0)
if os.Getenv("JUNHONG_AUDIT_RETENTION_SIMULATION_RESUME") == "true" {
if err := assertReadyToResume(ctx, db, monthStart, monthEnd); err != nil {
return err
}
} else {
if err := assertEmptyTarget(ctx, db, monthStart, monthEnd); err != nil {
return err
}
if err := seedSimulation(ctx, db, monthStart, monthEnd); err != nil {
return err
}
if err := archiveMonth(ctx, db, service, monthStart, monthEnd); err != nil {
return err
}
}
payload, _ := sonic.Marshal(taskapp.AuditMonthlyRetentionPayload{ArchiveMonth: simulationMonth})
dryRunTask := asynq.NewTask(constants.TaskTypeAuditMonthlyRetention, payload)
if err := taskapp.NewAuditMonthlyRetentionHandler(service, logger, false).Handle(ctx, dryRunTask); err != nil {
return fmt.Errorf("月度只读演练失败: %w", err)
}
dryRun, err := service.ValidateMonth(ctx, monthStart)
if err != nil {
return err
}
summary, err := collectBeforeCleanup(ctx, db, monthStart, monthEnd, dryRun)
if err != nil {
return err
}
if summary.CleanupMarkersBefore != 0 {
return fmt.Errorf("只归档模式写入了 %d 个清理断点", summary.CleanupMarkersBefore)
}
cleanupTask := asynq.NewTask(constants.TaskTypeAuditMonthlyRetention, payload)
if err := taskapp.NewAuditMonthlyRetentionHandler(service, logger, true).Handle(ctx, cleanupTask); err != nil {
return fmt.Errorf("隔离测试库物理清理演练失败: %w", err)
}
if err := collectAfterCleanup(ctx, db, monthStart, monthEnd, &summary); err != nil {
return err
}
if summary.TargetRowsAfterCleanup != 0 || summary.BoundaryRowsAfterCleanup != 6 || summary.CleanupMarkersAfter != int64(summary.Days*2) || !summary.RetentionAuditRecorded {
return fmt.Errorf("清理范围复核失败: target=%d boundary=%d markers=%d audit=%t",
summary.TargetRowsAfterCleanup, summary.BoundaryRowsAfterCleanup, summary.CleanupMarkersAfter, summary.RetentionAuditRecorded)
}
encoded, _ := sonic.MarshalIndent(summary, "", " ")
fmt.Println(string(encoded))
return nil
}
func assertReadyToResume(ctx context.Context, db *gorm.DB, start, end time.Time) error {
var events, integrations, runs, successful, final int64
if err := db.WithContext(ctx).Model(&model.AuditEvent{}).Where("created_at >= ? AND created_at < ?", start, end).Count(&events).Error; err != nil {
return err
}
if err := db.WithContext(ctx).Model(&model.IntegrationLog{}).Where("created_at >= ? AND created_at < ?", start, end).Count(&integrations).Error; err != nil {
return err
}
query := db.WithContext(ctx).Model(&model.LogArchiveRun{}).
Where("archive_date >= ? AND archive_date < ? AND instance_id = ?", start.Format(time.DateOnly), end.Format(time.DateOnly), simulationInstance)
if err := query.Count(&runs).Error; err != nil {
return err
}
if err := query.Where("status = ?", constants.ArchiveStatusSuccess).Count(&successful).Error; err != nil {
return err
}
if err := db.WithContext(ctx).Model(&model.LogArchiveRun{}).
Where("archive_date >= ? AND archive_date < ? AND instance_id = ? AND source = ? AND is_final", start.Format(time.DateOnly), end.Format(time.DateOnly), simulationInstance, constants.IntegrationArchiveSource).
Count(&final).Error; err != nil {
return err
}
days := int64(end.Sub(start).Hours() / 24)
if events != days || integrations != days || runs != days*2 || successful != runs || final != days {
return fmt.Errorf("现有仿真状态不可安全续跑: events=%d integrations=%d runs=%d successful=%d final=%d", events, integrations, runs, successful, final)
}
return nil
}
func assertEmptyTarget(ctx context.Context, db *gorm.DB, start, end time.Time) error {
var events, integrations, runs int64
if err := db.WithContext(ctx).Model(&model.AuditEvent{}).Where("created_at >= ? AND created_at < ?", start, end).Count(&events).Error; err != nil {
return err
}
if err := db.WithContext(ctx).Model(&model.IntegrationLog{}).Where("created_at >= ? AND created_at < ?", start, end).Count(&integrations).Error; err != nil {
return err
}
if err := db.WithContext(ctx).Model(&model.LogArchiveRun{}).Where("archive_date >= ? AND archive_date < ?", start.Format(time.DateOnly), end.Format(time.DateOnly)).Count(&runs).Error; err != nil {
return err
}
if events != 0 || integrations != 0 || runs != 0 {
return fmt.Errorf("仿真目标月已有数据,拒绝清理: events=%d integrations=%d runs=%d", events, integrations, runs)
}
return nil
}
func seedSimulation(ctx context.Context, db *gorm.DB, start, end time.Time) error {
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
for date := start; date.Before(end); date = date.AddDate(0, 0, 1) {
result := constants.IntegrationResultSuccess
if date.Equal(start) {
result = constants.IntegrationResultPending
}
if err := seedFact(tx, date.Add(12*time.Hour), date.Format("20060102"), result); err != nil {
return err
}
}
if err := seedFact(tx, start.Add(-time.Second), "before", constants.IntegrationResultSuccess); err != nil {
return err
}
return seedFact(tx, end, "after", constants.IntegrationResultSuccess)
})
}
func seedFact(tx *gorm.DB, at time.Time, suffix, integrationResult string) error {
marker := simulationPrefix + "-" + suffix
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(marker)))
event := model.AuditEvent{
EventID: "evt_" + marker, OccurredAt: at, Category: constants.AuditCategoryReliability,
ActionCode: constants.AuditActionLogRetentionCleanup, ActionName: "归档留存仿真", Summary: "归档留存边界仿真数据",
ActorKind: constants.AuditActorSystemTask, ActorID: simulationInstance, ActorName: "归档留存仿真",
Source: constants.AuditSourceWorker, ScopeType: constants.AuditScopePlatform,
Result: constants.AuditResultSuccess, RiskLevel: constants.AuditRiskLow,
Metadata: datatypes.JSON([]byte(`{"simulation":true}`)), ContentHash: hash, CreatedAt: at,
}
if err := tx.Create(&event).Error; err != nil {
return err
}
resourceID := marker
resource := model.AuditEventResource{
AuditEventID: event.ID, ResourceType: constants.AuditResourceLogArchiveMonth, ResourceID: &resourceID,
ResourceKey: marker, DisplayName: marker, Relation: constants.AuditResourceRelationPrimary,
Role: constants.AuditResourceRoleRetentionMonth, IdentitySnapshot: datatypes.JSON([]byte(`{"simulation":true}`)),
BeforeData: datatypes.JSON([]byte(`{}`)), AfterData: datatypes.JSON([]byte(`{}`)),
SubjectVisibility: constants.AuditSubjectInternalOnly, SubjectData: datatypes.JSON([]byte(`{}`)), CreatedAt: at,
}
if err := tx.Create(&resource).Error; err != nil {
return err
}
resourceType, resourceKey := constants.AuditResourceLogArchiveMonth, marker
integration := model.IntegrationLog{
IntegrationID: "int_" + marker, Provider: "retention_simulation", Direction: "outbound",
Operation: "archive_boundary", ResourceType: &resourceType, ResourceID: &resourceID, ResourceKey: &resourceKey,
Attempt: 1, Result: integrationResult, ContentHash: hash, RequestSummary: datatypes.JSON([]byte(`{"simulation":true}`)),
ResponseSummary: datatypes.JSON([]byte(`{}`)), Metadata: datatypes.JSON([]byte(`{"simulation":true}`)), CreatedAt: at, UpdatedAt: at,
}
return tx.Create(&integration).Error
}
func archiveMonth(ctx context.Context, db *gorm.DB, service *auditarchive.Service, start, end time.Time) error {
for date := start; date.Before(end); date = date.AddDate(0, 0, 1) {
if err := service.ArchiveDate(ctx, date); err != nil {
return err
}
if err := service.ArchiveIntegrationDate(ctx, date); err != nil {
return err
}
}
if err := db.WithContext(ctx).Model(&model.LogArchiveRun{}).
Where("source = ? AND archive_date = ? AND instance_id = ?", constants.AuditArchiveSource, start.Format(time.DateOnly), simulationInstance).
Update("sha256", strings.Repeat("0", 64)).Error; err != nil {
return err
}
if err := service.ArchiveDate(ctx, start); err != nil {
return fmt.Errorf("Audit 故障重试演练失败: %w", err)
}
if err := db.WithContext(ctx).Model(&model.IntegrationLog{}).
Where("integration_id = ?", "int_"+simulationPrefix+"-"+start.Format("20060102")).
Updates(map[string]any{"result": constants.IntegrationResultSuccess, "updated_at": time.Now()}).Error; err != nil {
return err
}
return service.FinalizeIntegrationMonth(ctx, start)
}
func collectBeforeCleanup(ctx context.Context, db *gorm.DB, start, end time.Time, dryRun auditarchive.RetentionResult) (simulationSummary, error) {
summary := simulationSummary{
Month: simulationMonth, Days: int(end.Sub(start).Hours() / 24),
DryRunEventCount: dryRun.EventCount, DryRunResourceCount: dryRun.ResourceCount,
DryRunIntegrationCount: dryRun.IntegrationCount, EstimatedCleanupBatches: dryRun.EstimatedBatches,
}
var compressed, uncompressed int64
err := db.WithContext(ctx).Model(&model.LogArchiveRun{}).
Where("archive_date >= ? AND archive_date < ? AND instance_id = ?", start.Format(time.DateOnly), end.Format(time.DateOnly), simulationInstance).
Select("COUNT(*) AS archive_runs, COUNT(*) FILTER (WHERE status = 'success') AS successful_runs, COUNT(*) FILTER (WHERE source = 'integration' AND is_final) AS final_integration_runs, COALESCE(MAX(revision), 0) AS max_revision, COALESCE(MAX(attempt_count), 0) AS max_attempt_count, COALESCE(SUM(compressed_bytes), 0) AS compressed, COALESCE(SUM(uncompressed_bytes), 0) AS uncompressed, COUNT(*) FILTER (WHERE cleanup_started_at IS NOT NULL OR cleaned_at IS NOT NULL) AS cleanup_markers_before").
Row().Scan(&summary.ArchiveRuns, &summary.SuccessfulRuns, &summary.FinalIntegrationRuns, &summary.MaxRevision, &summary.MaxAttemptCount, &compressed, &uncompressed, &summary.CleanupMarkersBefore)
if err != nil {
return summary, err
}
if uncompressed > 0 {
summary.CompressionRatio = float64(compressed) / float64(uncompressed)
}
return summary, nil
}
func collectAfterCleanup(ctx context.Context, db *gorm.DB, start, end time.Time, summary *simulationSummary) error {
var events, resources, integrations int64
if err := db.WithContext(ctx).Model(&model.AuditEvent{}).Where("created_at >= ? AND created_at < ?", start, end).Count(&events).Error; err != nil {
return err
}
if err := db.WithContext(ctx).Model(&model.AuditEventResource{}).Joins("JOIN tb_audit_event e ON e.id = tb_audit_event_resource.audit_event_id").Where("e.created_at >= ? AND e.created_at < ?", start, end).Count(&resources).Error; err != nil {
return err
}
if err := db.WithContext(ctx).Model(&model.IntegrationLog{}).Where("created_at >= ? AND created_at < ?", start, end).Count(&integrations).Error; err != nil {
return err
}
summary.TargetRowsAfterCleanup = events + resources + integrations
if err := db.WithContext(ctx).Model(&model.AuditEvent{}).Where("event_id IN ?", []string{"evt_" + simulationPrefix + "-before", "evt_" + simulationPrefix + "-after"}).Count(&events).Error; err != nil {
return err
}
if err := db.WithContext(ctx).Model(&model.AuditEventResource{}).Where("resource_key IN ?", []string{simulationPrefix + "-before", simulationPrefix + "-after"}).Count(&resources).Error; err != nil {
return err
}
if err := db.WithContext(ctx).Model(&model.IntegrationLog{}).Where("integration_id IN ?", []string{"int_" + simulationPrefix + "-before", "int_" + simulationPrefix + "-after"}).Count(&integrations).Error; err != nil {
return err
}
summary.BoundaryRowsAfterCleanup = events + resources + integrations
if err := db.WithContext(ctx).Model(&model.LogArchiveRun{}).
Where("archive_date >= ? AND archive_date < ? AND instance_id = ? AND cleanup_started_at IS NOT NULL AND cleaned_at IS NOT NULL", start.Format(time.DateOnly), end.Format(time.DateOnly), simulationInstance).
Count(&summary.CleanupMarkersAfter).Error; err != nil {
return err
}
var auditCount int64
if err := db.WithContext(ctx).Model(&model.AuditEvent{}).Where("event_id = ?", "evt_retention_"+strings.ReplaceAll(simulationMonth, "-", "_")).Count(&auditCount).Error; err != nil {
return err
}
summary.RetentionAuditRecorded = auditCount == 1
return nil
}