package logfile import ( "strconv" "strings" "time" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) // ErrDisabled is returned by ParseLogFileSpec when the spec is "none". var ErrDisabled = errors.New("log file disabled") // LogFileSpec describes a log file destination. type LogFileSpec struct { // Level is the logrus level threshold. Level logrus.Level // Format is the output format ("text" or "json"). Format string // Path is the expanded file path (templates already resolved). Path string } // ParseLogFileSpec parses a log file spec string into a LogFileSpec. // // Spec syntax (semicolon-delimited key=value pairs): // - Full form: level=DEBUG;format=json;path=./.bldr/logs/{ts}.log // - Short form: ./.bldr/logs/{ts}.log (path only, defaults apply) // - "none" -> returns ErrDisabled // // Default level: DEBUG. Default format: text. // Unknown keys produce an error. Missing path produces an error. func ParseLogFileSpec(spec string, ts time.Time) (LogFileSpec, error) { // Reject empty or explicitly disabled log destinations. spec = strings.TrimSpace(spec) if spec == "" { return LogFileSpec{}, errors.New("empty log file spec") } if spec == "none" { return LogFileSpec{}, ErrDisabled } // Prepare the default level and format for the log destination. result := LogFileSpec{ Level: logrus.DebugLevel, Format: "text", } // Check if this is a short form (no semicolons and no '=' sign). if !strings.Contains(spec, ";") && !strings.Contains(spec, "=") { result.Path = ExpandTemplate(spec, ts) return result, nil } // Parse the configured fields into the log destination. fields := strings.Split(spec, ";") for i, field := range fields { // Ignore blank fields in the log specification. field = strings.TrimSpace(field) if field == "" { continue } // Separate each field value or accept a trailing bare path. before, after, ok := strings.Cut(field, "=") if !ok { // Last field without '=' is treated as path. if i == len(fields)-1 { result.Path = ExpandTemplate(field, ts) continue } return LogFileSpec{}, errors.New("invalid field " + strconv.Quote(field) + ": missing '='") } // Normalize the log field key and value. key := strings.TrimSpace(before) val := strings.TrimSpace(after) // Apply the requested level, format, or expanded path. switch key { case "level": lvl, err := logrus.ParseLevel(val) if err != nil { return LogFileSpec{}, errors.Wrap(err, "invalid level "+strconv.Quote(val)) } result.Level = lvl case "format": if val != "text" && val != "json" { return LogFileSpec{}, errors.New("invalid format " + strconv.Quote(val) + ": must be \"text\" or \"json\"") } result.Format = val case "path": result.Path = ExpandTemplate(val, ts) default: return LogFileSpec{}, errors.New("unknown key " + strconv.Quote(key)) } } // Require a path before returning the log destination. if result.Path == "" { return LogFileSpec{}, errors.New("missing path in log file spec") } return result, nil }