package runtimelogs import ( "context" "fmt" "evobgp/internal/store" ) const autoSchedulerActor = "auto:scheduler" // FileEstimate describes whether a log file would be cleaned by auto policy. type FileEstimate struct { Filename string `json:"filename"` SizeBytes int64 `json:"size_bytes"` WouldCleanup bool `json:"would_cleanup"` SkipReason string `json:"skip_reason,omitempty"` } // AutoCleanupDeps wires FS cleanup with audit persistence. type AutoCleanupDeps struct { Service *Service Store store.Backend TenantID string } // EstimateAutoCleanup lists files that exceed the configured size threshold. func EstimateAutoCleanup(svc *Service, policy AutoPolicy) ([]FileEstimate, error) { if svc == nil || !svc.Available() { return nil, ErrUnavailable } files, err := svc.ListFiles() if err != nil { return nil, err } out := make([]FileEstimate, 0, len(files)) for _, f := range files { est := FileEstimate{ Filename: f.Name, SizeBytes: f.SizeBytes, } if f.SizeBytes <= policy.MaxFileBytes { est.SkipReason = "under_threshold" } else if f.SizeBytes > MaxCleanupBytes { est.SkipReason = "too_large" } else { est.WouldCleanup = true } out = append(out, est) } return out, nil } // RunAutoCleanup applies auto policy to eligible files and writes audit rows. func RunAutoCleanup(ctx context.Context, deps AutoCleanupDeps, policy AutoPolicy, dryRun bool, trigger string) (map[string]any, error) { if deps.Service == nil || !deps.Service.Available() { return nil, ErrUnavailable } if deps.Store == nil || deps.TenantID == "" { return nil, fmt.Errorf("runtimelogs: store tenant required for auto cleanup") } _ = ctx estimates, err := EstimateAutoCleanup(deps.Service, policy) if err != nil { return nil, err } detailBase := map[string]any{ "trigger": trigger, "dry_run": dryRun, "max_file_bytes": policy.MaxFileBytes, "mode": policy.Mode, "files_processed": 0, } cleaned := make([]map[string]any, 0) skipped := make([]map[string]any, 0) for _, est := range estimates { if !est.WouldCleanup { if est.SkipReason != "" { skipped = append(skipped, map[string]any{ "filename": est.Filename, "reason": est.SkipReason, "size": est.SizeBytes, }) } continue } if dryRun { cleaned = append(cleaned, map[string]any{ "filename": est.Filename, "size": est.SizeBytes, "dry_run": true, }) continue } sizeBefore, sizeAfter, err := deps.Service.Cleanup(est.Filename, policy.Mode) if err != nil { skipped = append(skipped, map[string]any{ "filename": est.Filename, "reason": err.Error(), "size": est.SizeBytes, }) continue } auditDetail := map[string]any{ "trigger": trigger, "mode": policy.Mode, } auditID, err := deps.Store.AppendRuntimeLogCleanupAudit( deps.TenantID, autoSchedulerActor, est.Filename, policy.Mode, sizeBefore, sizeAfter, auditDetail) if err != nil { return nil, fmt.Errorf("runtimelogs: audit: %w", err) } entry := map[string]any{ "filename": est.Filename, "audit_id": auditID, "size_before": sizeBefore, "action": policy.Mode, } if sizeAfter != nil { entry["size_after"] = *sizeAfter } cleaned = append(cleaned, entry) } detailBase["files_processed"] = len(cleaned) return map[string]any{ "dry_run": dryRun, "trigger": trigger, "policy": policySnapshot(policy), "cleaned": cleaned, "skipped": skipped, "cleaned_count": len(cleaned), "skipped_count": len(skipped), }, nil } func policySnapshot(p AutoPolicy) map[string]any { return map[string]any{ "enabled": p.Enabled, "max_file_bytes": p.MaxFileBytes, "schedule": p.Schedule, "mode": p.Mode, } }