feat(runtime-logs): enhance auto-cleanup features and documentation
CI / changes (push) Successful in 9s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 26s
CI / web (push) Successful in 33s
CI / go (push) Successful in 56s
CI / bird2 (push) Successful in 14s
CI / release (push) Successful in 20s
CI / changes (push) Successful in 9s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 26s
CI / web (push) Successful in 33s
CI / go (push) Successful in 56s
CI / bird2 (push) Successful in 14s
CI / release (push) Successful in 20s
Added new endpoints for estimating and executing runtime log auto-cleanup based on tenant settings. Introduced configuration options for auto-cleanup policies, including scheduling and file size limits. Updated the API documentation and UI components to reflect these changes, improving user interaction with runtime log management. Enhanced error handling and added new UI elements for better visibility of audit logs and cleanup actions.
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package runtimelogs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"evobgp/internal/store"
|
||||
)
|
||||
|
||||
func TestEstimateAndRunAutoCleanup(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
small := filepath.Join(dir, "small.log")
|
||||
large := filepath.Join(dir, "large.log")
|
||||
if err := os.WriteFile(small, []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload := make([]byte, 2*1024*1024)
|
||||
if err := os.WriteFile(large, payload, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
svc := NewService(Config{RootDir: dir, ServiceName: ServiceNameAll})
|
||||
policy := AutoPolicy{
|
||||
Enabled: true,
|
||||
MaxFileBytes: 1024 * 1024,
|
||||
Mode: store.RuntimeLogCleanupTruncate,
|
||||
}
|
||||
|
||||
est, err := EstimateAutoCleanup(svc, policy)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(est) != 2 {
|
||||
t.Fatalf("estimates=%d", len(est))
|
||||
}
|
||||
var would int
|
||||
for _, e := range est {
|
||||
if e.WouldCleanup {
|
||||
would++
|
||||
if e.Filename != "large.log" {
|
||||
t.Fatalf("unexpected cleanup target %q", e.Filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
if would != 1 {
|
||||
t.Fatalf("would=%d", would)
|
||||
}
|
||||
|
||||
mem := store.NewMemory()
|
||||
tenant := "tenant-a"
|
||||
result, err := RunAutoCleanup(context.Background(), AutoCleanupDeps{
|
||||
Service: svc,
|
||||
Store: mem,
|
||||
TenantID: tenant,
|
||||
}, policy, false, "test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result["cleaned_count"] != 1 {
|
||||
t.Fatalf("cleaned=%v", result["cleaned_count"])
|
||||
}
|
||||
st, err := os.Stat(large)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if st.Size() != 0 {
|
||||
t.Fatalf("expected truncated large.log, size=%d", st.Size())
|
||||
}
|
||||
items, _, _, err := mem.ListRuntimeLogCleanupAudit(tenant, "", 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(items) != 1 {
|
||||
t.Fatalf("audit rows=%d", len(items))
|
||||
}
|
||||
if items[0].ActorPrefix != autoSchedulerActor {
|
||||
t.Fatalf("actor=%q", items[0].ActorPrefix)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package runtimelogs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"evobgp/internal/store"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
// global_settings keys for runtime log auto-cleanup.
|
||||
const (
|
||||
KeyAutoEnabled = "runtime_logs_auto_enabled"
|
||||
KeyMaxFileMB = "runtime_logs_max_file_mb"
|
||||
KeyAutoSchedule = "runtime_logs_auto_schedule"
|
||||
KeyAutoMode = "runtime_logs_auto_mode"
|
||||
DefaultMaxFileMB = 128
|
||||
DefaultSchedule = "0 */6 * * *"
|
||||
)
|
||||
|
||||
// AutoPolicy is tenant KV configuration for scheduled FS log cleanup.
|
||||
type AutoPolicy struct {
|
||||
Enabled bool
|
||||
MaxFileBytes int64
|
||||
Schedule string
|
||||
Mode string
|
||||
}
|
||||
|
||||
// DefaultAutoPolicy returns policy defaults when settings are unset.
|
||||
func DefaultAutoPolicy() AutoPolicy {
|
||||
return AutoPolicy{
|
||||
Enabled: false,
|
||||
MaxFileBytes: int64(DefaultMaxFileMB) * 1024 * 1024,
|
||||
Schedule: DefaultSchedule,
|
||||
Mode: store.RuntimeLogCleanupTruncate,
|
||||
}
|
||||
}
|
||||
|
||||
// PolicyFromSettings reads auto-cleanup policy from tenant global_settings KV.
|
||||
func PolicyFromSettings(settings map[string]any) AutoPolicy {
|
||||
p := DefaultAutoPolicy()
|
||||
if settings == nil {
|
||||
return p
|
||||
}
|
||||
if v, ok := settings[KeyAutoEnabled]; ok {
|
||||
p.Enabled = parseBoolSetting(v)
|
||||
}
|
||||
if mb := parseIntSetting(settings[KeyMaxFileMB]); mb > 0 {
|
||||
p.MaxFileBytes = int64(ClampMaxFileMB(mb)) * 1024 * 1024
|
||||
}
|
||||
if s := parseStringSetting(settings[KeyAutoSchedule]); s != "" {
|
||||
p.Schedule = s
|
||||
}
|
||||
if m := parseStringSetting(settings[KeyAutoMode]); store.ValidRuntimeLogCleanupAction(m) {
|
||||
p.Mode = m
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// ClampMaxFileMB normalizes max file size to 1..512 MiB.
|
||||
func ClampMaxFileMB(mb int) int {
|
||||
if mb <= 0 {
|
||||
return DefaultMaxFileMB
|
||||
}
|
||||
if mb < 1 {
|
||||
return 1
|
||||
}
|
||||
if mb > 512 {
|
||||
return 512
|
||||
}
|
||||
return mb
|
||||
}
|
||||
|
||||
// ValidAutoSchedule reports whether schedule is a valid 5-field UTC cron expression.
|
||||
func ValidAutoSchedule(schedule string) bool {
|
||||
schedule = strings.TrimSpace(schedule)
|
||||
if schedule == "" {
|
||||
return false
|
||||
}
|
||||
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
||||
_, err := parser.Parse(schedule)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// ValidateRuntimeLogsSettingsPatch normalizes and validates runtime log settings in a PATCH body.
|
||||
// Returns false if any present key is invalid.
|
||||
func ValidateRuntimeLogsSettingsPatch(body map[string]any) bool {
|
||||
if raw, ok := body[KeyAutoEnabled]; ok && raw != nil {
|
||||
v, ok := normalizeBoolSetting(raw)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
body[KeyAutoEnabled] = v
|
||||
}
|
||||
if raw, ok := body[KeyMaxFileMB]; ok && raw != nil {
|
||||
mb, ok := parsePatchInt(raw)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
mb = ClampMaxFileMB(mb)
|
||||
body[KeyMaxFileMB] = mb
|
||||
}
|
||||
if raw, ok := body[KeyAutoSchedule]; ok && raw != nil {
|
||||
s := parseStringSetting(raw)
|
||||
if !ValidAutoSchedule(s) {
|
||||
return false
|
||||
}
|
||||
body[KeyAutoSchedule] = s
|
||||
}
|
||||
if raw, ok := body[KeyAutoMode]; ok && raw != nil {
|
||||
m := parseStringSetting(raw)
|
||||
if !store.ValidRuntimeLogCleanupAction(m) {
|
||||
return false
|
||||
}
|
||||
body[KeyAutoMode] = m
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func parseBoolSetting(v any) bool {
|
||||
switch x := v.(type) {
|
||||
case bool:
|
||||
return x
|
||||
case string:
|
||||
s := strings.TrimSpace(strings.ToLower(x))
|
||||
return s == "1" || s == "true" || s == "yes"
|
||||
case float64:
|
||||
return x != 0
|
||||
case int:
|
||||
return x != 0
|
||||
case int64:
|
||||
return x != 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeBoolSetting(v any) (bool, bool) {
|
||||
switch x := v.(type) {
|
||||
case bool:
|
||||
return x, true
|
||||
case string:
|
||||
s := strings.TrimSpace(strings.ToLower(x))
|
||||
switch s {
|
||||
case "1", "true", "yes":
|
||||
return true, true
|
||||
case "0", "false", "no":
|
||||
return false, true
|
||||
default:
|
||||
return false, false
|
||||
}
|
||||
case float64:
|
||||
if x != 0 && x != 1 {
|
||||
return false, false
|
||||
}
|
||||
return x != 0, true
|
||||
case int:
|
||||
if x != 0 && x != 1 {
|
||||
return false, false
|
||||
}
|
||||
return x != 0, true
|
||||
case int64:
|
||||
if x != 0 && x != 1 {
|
||||
return false, false
|
||||
}
|
||||
return x != 0, true
|
||||
default:
|
||||
return false, false
|
||||
}
|
||||
}
|
||||
|
||||
func parseIntSetting(v any) int {
|
||||
switch x := v.(type) {
|
||||
case float64:
|
||||
return int(x)
|
||||
case int:
|
||||
return x
|
||||
case int64:
|
||||
return int(x)
|
||||
case string:
|
||||
n, err := strconv.Atoi(strings.TrimSpace(x))
|
||||
if err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func parsePatchInt(v any) (int, bool) {
|
||||
switch x := v.(type) {
|
||||
case float64:
|
||||
if x != float64(int(x)) {
|
||||
return 0, false
|
||||
}
|
||||
n := int(x)
|
||||
return n, n >= 1 && n <= 512
|
||||
case int:
|
||||
return x, x >= 1 && x <= 512
|
||||
case int64:
|
||||
n := int(x)
|
||||
return n, n >= 1 && n <= 512
|
||||
case string:
|
||||
n, err := strconv.Atoi(strings.TrimSpace(x))
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return n, n >= 1 && n <= 512
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func parseStringSetting(v any) string {
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(x)
|
||||
default:
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package runtimelogs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"evobgp/internal/store"
|
||||
)
|
||||
|
||||
func TestPolicyFromSettingsDefaults(t *testing.T) {
|
||||
p := PolicyFromSettings(nil)
|
||||
if p.Enabled {
|
||||
t.Fatal("expected disabled by default")
|
||||
}
|
||||
if p.MaxFileBytes != int64(DefaultMaxFileMB)*1024*1024 {
|
||||
t.Fatalf("max bytes=%d", p.MaxFileBytes)
|
||||
}
|
||||
if p.Schedule != DefaultSchedule {
|
||||
t.Fatalf("schedule=%q", p.Schedule)
|
||||
}
|
||||
if p.Mode != store.RuntimeLogCleanupTruncate {
|
||||
t.Fatalf("mode=%q", p.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClampMaxFileMB(t *testing.T) {
|
||||
if ClampMaxFileMB(0) != DefaultMaxFileMB {
|
||||
t.Fatal("zero should default")
|
||||
}
|
||||
if ClampMaxFileMB(999) != 512 {
|
||||
t.Fatal("cap 512")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRuntimeLogsSettingsPatch(t *testing.T) {
|
||||
body := map[string]any{
|
||||
KeyAutoEnabled: true,
|
||||
KeyMaxFileMB: 64,
|
||||
KeyAutoSchedule: "0 3 * * *",
|
||||
KeyAutoMode: "truncate",
|
||||
}
|
||||
if !ValidateRuntimeLogsSettingsPatch(body) {
|
||||
t.Fatal("expected valid patch")
|
||||
}
|
||||
if body[KeyMaxFileMB] != 64 {
|
||||
t.Fatalf("max mb=%v", body[KeyMaxFileMB])
|
||||
}
|
||||
|
||||
bad := map[string]any{KeyAutoSchedule: "not a cron"}
|
||||
if ValidateRuntimeLogsSettingsPatch(bad) {
|
||||
t.Fatal("expected invalid cron")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidAutoSchedule(t *testing.T) {
|
||||
if !ValidAutoSchedule("0 */6 * * *") {
|
||||
t.Fatal("expected valid schedule")
|
||||
}
|
||||
if ValidAutoSchedule("invalid") {
|
||||
t.Fatal("expected invalid schedule")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package runtimelogs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"evobgp/internal/store"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
// SchedulerDeps configures the runtime log auto-cleanup background scheduler.
|
||||
type SchedulerDeps struct {
|
||||
Service *Service
|
||||
Store store.Backend
|
||||
PolicyTenant string
|
||||
}
|
||||
|
||||
// ResolvePolicyTenant picks the tenant whose settings drive auto-cleanup.
|
||||
func ResolvePolicyTenant(st store.Backend, explicit string) (string, error) {
|
||||
explicit = strings.TrimSpace(explicit)
|
||||
if explicit != "" {
|
||||
return explicit, nil
|
||||
}
|
||||
if st == nil {
|
||||
return "", fmt.Errorf("runtimelogs: store required")
|
||||
}
|
||||
ids, err := st.ListTenantIDs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, id := range ids {
|
||||
settings, err := st.ListGlobalSettings(id)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if PolicyFromSettings(settings).Enabled {
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
return ids[0], nil
|
||||
}
|
||||
return "", fmt.Errorf("runtimelogs: no tenants configured")
|
||||
}
|
||||
|
||||
// StartAutoCleanupScheduler runs periodic auto-cleanup on evobgp-all when FS is available.
|
||||
func StartAutoCleanupScheduler(ctx context.Context, deps SchedulerDeps, tick time.Duration) {
|
||||
if deps.Service == nil || !deps.Service.Available() {
|
||||
log.Printf("runtimelogs: auto-cleanup scheduler disabled (FS unavailable)")
|
||||
return
|
||||
}
|
||||
if deps.Store == nil {
|
||||
log.Printf("runtimelogs: auto-cleanup scheduler disabled (no store)")
|
||||
return
|
||||
}
|
||||
if tick <= 0 {
|
||||
tick = 30 * time.Second
|
||||
}
|
||||
|
||||
go func() {
|
||||
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
||||
var mu sync.Mutex
|
||||
lastFired := map[string]time.Time{}
|
||||
|
||||
t := time.NewTicker(tick)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
tenantID, err := ResolvePolicyTenant(deps.Store, deps.PolicyTenant)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
settings, err := deps.Store.ListGlobalSettings(tenantID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
policy := PolicyFromSettings(settings)
|
||||
if !policy.Enabled {
|
||||
continue
|
||||
}
|
||||
sched, err := parser.Parse(policy.Schedule)
|
||||
if err != nil {
|
||||
log.Printf("runtimelogs: invalid cron %q: %v", policy.Schedule, err)
|
||||
continue
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
mu.Lock()
|
||||
prev := lastFired[policy.Schedule]
|
||||
if prev.IsZero() {
|
||||
prev = now.Add(-time.Minute)
|
||||
}
|
||||
next := sched.Next(prev)
|
||||
if next.After(now) {
|
||||
mu.Unlock()
|
||||
continue
|
||||
}
|
||||
slot := next.Unix() / 60
|
||||
if lf, ok := lastFired[policy.Schedule]; ok && lf.Unix()/60 == slot {
|
||||
mu.Unlock()
|
||||
continue
|
||||
}
|
||||
lastFired[policy.Schedule] = next
|
||||
mu.Unlock()
|
||||
|
||||
_, err = RunAutoCleanup(ctx, AutoCleanupDeps{
|
||||
Service: deps.Service,
|
||||
Store: deps.Store,
|
||||
TenantID: tenantID,
|
||||
}, policy, false, "scheduler")
|
||||
if err != nil {
|
||||
log.Printf("runtimelogs: auto-cleanup run failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
log.Printf("runtimelogs: auto-cleanup scheduler started (tick=%s)", tick)
|
||||
}
|
||||
Reference in New Issue
Block a user