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)) } }