package logfile import ( "os" "path/filepath" "strconv" "strings" "time" "github.com/sirupsen/logrus" ) // AutoDefaultEnvVar is the environment variable that controls the file // logging spec. When set to a non-empty value, including "none", the user's // configuration wins and BuildAutoDefaultSpec returns no spec. const AutoDefaultEnvVar = "BLDR_LOG_FILE" // DefaultRetention is the on-disk retention duration applied when no // project-scoped override resolves to a positive duration. const DefaultRetention = 7 * 24 * time.Hour // BuildAutoDefaultSpec returns the auto-default LogFileSpec for an // entrypoint that has no explicit BLDR_LOG_FILE configuration. // // The resolved spec writes DEBUG-level text records to // /logs/{ts}.log, where {ts} is expanded against now. // If BLDR_LOG_FILE is set to a non-empty value (including "none"), the user's // configuration takes precedence and this function returns ok=false so the // caller defers to the existing parsing path. Empty values behave like the // variable is unset and still get the auto-default. // // Callers receive the spec but are responsible for opening the file via // AttachLogFiles and pruning old logs via PruneOldLogs. func BuildAutoDefaultSpec(storageRoot string, now time.Time) (LogFileSpec, bool) { if raw, ok := os.LookupEnv(AutoDefaultEnvVar); ok && strings.TrimSpace(raw) != "" { return LogFileSpec{}, false } if storageRoot == "" { return LogFileSpec{}, false } path := filepath.Join(storageRoot, "logs", "{ts}.log") return LogFileSpec{ Level: logrus.DebugLevel, Format: "text", Path: ExpandTemplate(path, now), }, true } // ResolveRetention returns the configured on-disk log retention duration. // It reads the project-scoped retention env var (e.g. // SPACEWAVE_LOG_RETENTION_DAYS) and falls back to DefaultRetention when // the variable is unset, blank, non-positive, or unparseable as a // non-negative integer. // // When the variable is set but cannot be parsed, the returned warn message // is non-empty so callers can emit one logrus.Warn at startup. A // successful parse always returns warn == "". func ResolveRetention(env, raw string) (dur time.Duration, warn string) { // Use the default retention when the environment value is blank. raw = strings.TrimSpace(raw) if raw == "" { return DefaultRetention, "" } // Parse the retention days and report malformed configuration. days, err := strconv.Atoi(raw) if err != nil { return DefaultRetention, env + "=" + strconv.Quote(raw) + " is not a non-negative integer; using default retention" } // Keep the default retention for non-positive durations. if days <= 0 { return DefaultRetention, "" } return time.Duration(days) * 24 * time.Hour, "" } // ResolveLogLevel walks envChain in order, returning the first parseable // logrus level found. Empty / unset values are skipped; a value that fails // to parse is also skipped (callers receive fallback in that case). // Returns fallback when no env var resolves. func ResolveLogLevel(envChain []string, fallback logrus.Level) logrus.Level { for _, name := range envChain { raw := strings.TrimSpace(os.Getenv(name)) if raw == "" { continue } lvl, err := logrus.ParseLevel(raw) if err != nil { continue } return lvl } return fallback } // EnableAutoDefault enables file logging at the storage-root-relative // default location when BLDR_LOG_FILE is unset. It prunes stale logs, // attaches the file hook, and raises the logger level so console // filtering applies via EnsureLoggerLevel. // // Returns a cleanup function that closes the file hook (nil when no // hook was attached). When BLDR_LOG_FILE is set or storageRoot is // empty, this is a no-op and returns (nil, nil). // // retentionEnv names the project-scoped retention env var (e.g. // "SPACEWAVE_LOG_RETENTION_DAYS"); pass "" to skip the override and // always use DefaultRetention. logger receives the retention warning // (if any) before the hook is attached so it lands in stderr but not // in the about-to-be-created file. func EnableAutoDefault( logger *logrus.Logger, storageRoot string, retentionEnv string, now time.Time, ) (func(), error) { // Resolve the default log destination unless explicit configuration takes precedence. spec, ok := BuildAutoDefaultSpec(storageRoot, now) if !ok { return nil, nil } // Resolve the project retention override and report its warning. retention := DefaultRetention if retentionEnv != "" { dur, warn := ResolveRetention(retentionEnv, os.Getenv(retentionEnv)) retention = dur if warn != "" { logger.Warn(warn) } } // Prune expired logs before opening the new log file. logsDir := filepath.Dir(spec.Path) if _, err := PruneOldLogs(logsDir, retention, DefaultKeepLogs, now); err != nil { logger.WithError(err).Warn("failed to prune old logs") } // Attach the default file hook and enable its requested verbosity. cleanup, err := AttachLogFiles(logger, []LogFileSpec{spec}) if err != nil { return nil, err } EnsureLoggerLevel(logger, []LogFileSpec{spec}) return cleanup, nil }