// Package log provides leveled logging with progress output helpers // on top of apex/log and pterm. package log import ( "fmt" "io" "os" "path/filepath" "runtime" "sync" "github.com/apex/log" acli "github.com/apex/log/handlers/cli" "github.com/davecgh/go-spew/spew" "github.com/pterm/pterm" ) // Level represents log severity levels. // Lower values are more verbose. type Level int const ( // DebugLevel is for low-level tracing and structure inspection DebugLevel Level = iota // VerboseLevel is for detailed operational info (file listings, etc) VerboseLevel // InfoLevel is for operational summaries (default) InfoLevel // WarnLevel is for warnings WarnLevel // ErrorLevel is for errors ErrorLevel // FatalLevel is for fatal errors FatalLevel ) func (l Level) String() string { switch l { case DebugLevel: return "debug" case VerboseLevel: return "verbose" case InfoLevel: return "info" case WarnLevel: return "warn" case ErrorLevel: return "error" case FatalLevel: return "fatal" default: return "unknown" } } // callerSkip is the runtime.Caller stack depth from the public Debug // helpers to the caller of the log package. const callerSkip = 2 //nolint:gochecknoglobals // package-level logger state by design var ( // mu protects the output writers and level mu sync.RWMutex // stdout is the writer for progress output stdout io.Writer = os.Stdout // stderr is the writer for log output stderr io.Writer = os.Stderr // currentLevel is our log level (includes Verbose) currentLevel = InfoLevel ) // SetOutput configures the output writers for the log package. // stdout is used for progress output, stderr is used for log messages. func SetOutput(out, err io.Writer) { mu.Lock() defer mu.Unlock() stdout = out stderr = err pterm.SetDefaultOutput(out) } // GetStdout returns the configured stdout writer. func GetStdout() io.Writer { mu.RLock() defer mu.RUnlock() return stdout } // GetStderr returns the configured stderr writer. func GetStderr() io.Writer { mu.RLock() defer mu.RUnlock() return stderr } // DisableStyling turns off colors and styling for terminal output. func DisableStyling() { pterm.DisableColor() pterm.DisableStyling() pterm.Debug.Prefix.Text = "" pterm.Info.Prefix.Text = "" pterm.Success.Prefix.Text = "" pterm.Warning.Prefix.Text = "" pterm.Error.Prefix.Text = "" pterm.Fatal.Prefix.Text = "" } // Init initializes the logger with the CLI handler and default log level. // // It reconfigures the process-global apex/log logger under the write lock so // the global is never mutated while another goroutine holds the read lock to // read it in emit. Without this, parallel callers (e.g. the test suite) race // Init's SetLevel/SetHandler against concurrent log calls. func Init() { mu.Lock() defer mu.Unlock() log.SetHandler(acli.New(stderr)) log.SetLevel(log.DebugLevel) // Let apex/log pass everything; we filter ourselves } // isEnabled returns true if messages at the given level should be logged. func isEnabled(l Level) bool { mu.RLock() defer mu.RUnlock() return l >= currentLevel } // emit calls fn while holding the read lock if messages at level l are // enabled. Holding the read lock across the apex/log call keeps the global // logger from being read while Init reconfigures it under the write lock. func emit(l Level, fn func()) { mu.RLock() defer mu.RUnlock() if l >= currentLevel { fn() } } // Fatalf logs a formatted message at fatal level. func Fatalf(format string, args ...any) { emit(FatalLevel, func() { log.Fatalf(format, args...) }) } // Fatal logs a message at fatal level. func Fatal(arg string) { emit(FatalLevel, func() { log.Fatal(arg) }) } // Errorf logs a formatted message at error level. func Errorf(format string, args ...any) { emit(ErrorLevel, func() { log.Errorf(format, args...) }) } // Error logs a message at error level. func Error(arg string) { emit(ErrorLevel, func() { log.Error(arg) }) } // Warnf logs a formatted message at warn level. func Warnf(format string, args ...any) { emit(WarnLevel, func() { log.Warnf(format, args...) }) } // Warn logs a message at warn level. func Warn(arg string) { emit(WarnLevel, func() { log.Warn(arg) }) } // Infof logs a formatted message at info level. func Infof(format string, args ...any) { emit(InfoLevel, func() { log.Infof(format, args...) }) } // Info logs a message at info level. func Info(arg string) { emit(InfoLevel, func() { log.Info(arg) }) } // Verbosef logs a formatted message at verbose level. func Verbosef(format string, args ...any) { emit(VerboseLevel, func() { log.Infof(format, args...) }) } // Verbose logs a message at verbose level. func Verbose(arg string) { emit(VerboseLevel, func() { log.Info(arg) }) } // Debugf logs a formatted message at debug level with caller location. func Debugf(format string, args ...any) { if isEnabled(DebugLevel) { DebugReal(fmt.Sprintf(format, args...), callerSkip) } } // Debug logs a message at debug level with caller location. func Debug(arg string) { if isEnabled(DebugLevel) { DebugReal(arg, callerSkip) } } // DebugReal logs at debug level with caller info from the specified stack depth. func DebugReal(arg string, cs int) { mu.RLock() defer mu.RUnlock() if DebugLevel < currentLevel { return } _, callerFile, callerLine, ok := runtime.Caller(cs) if !ok { return } tag := fmt.Sprintf("%s:%d: ", filepath.Base(callerFile), callerLine) log.Debug(tag + arg) } // Dump logs a spew dump of the arguments at debug level. func Dump(args ...any) { if isEnabled(DebugLevel) { DebugReal(spew.Sdump(args...), callerSkip) } } // EnableDebugLogging sets the log level to debug. func EnableDebugLogging() { SetLevel(DebugLevel) } // VerbosityStepsToLogLevel converts a -v count to a log level. // 0 returns InfoLevel, 1 returns VerboseLevel, 2+ returns DebugLevel. func VerbosityStepsToLogLevel(l int) Level { switch l { case 0: return InfoLevel case 1: return VerboseLevel default: return DebugLevel } } // SetLevelFromVerbosity sets the log level based on -v flag count. func SetLevelFromVerbosity(l int) { SetLevel(VerbosityStepsToLogLevel(l)) } // SetLevel sets the global log level. func SetLevel(l Level) { mu.Lock() defer mu.Unlock() currentLevel = l } // GetLevel returns the current log level. func GetLevel() Level { mu.RLock() defer mu.RUnlock() return currentLevel } // Progressf prints a progress message that overwrites the current line. // Use ProgressDone() when progress is complete to move to the next line. func Progressf(format string, args ...any) { pterm.Printf("\r"+format, args...) } // ProgressDone clears the progress line when progress is complete. func ProgressDone() { // Clear the line with spaces and return to beginning pterm.Print("\r\033[K") }